IP 地址
IP 地址在 HTTP 中起着基础性的作用;它用于访问控制、审计、基于地理位置的访问分析等。Slim 为此提供了方便的方法 Context#RealIP() 轻松获取。
但是,从请求中检索真实 IP 地址并非易事,尤其是在我们使用了某些代理技术时。在这种情况下,需要在 HTTP 层将代理程序中的真实 IP 中继到我们的应用程序上,所以我们不能无条件地信任 HTTP 报头,否则可能得到虚假地址,产生安全风险!
若要可靠且安全地检索 IP 地址,必须让应用程序了解基础架构的整个体系结构。在 Slim 中,可以通过适当配置 Slim#IPExtractor 来完成,本指南向您展示了原因和方法。
默认行为
如果我们不显式地为 Slim 实例属性 Slim#IPExtractor 设置值,Context#RealIP() 会使用以下回退逻辑:
- 首先检查
X-Forwarded-For标头,取第一个 IP 地址 - 如果没有,检查
X-Real-IP标头 - 最后回退到
Request.RemoteAddr
func (c *contextImpl) RealIP() string { if c.slim.IPExtractor != nil { return c.slim.IPExtractor(c.request) } // 回退到默认行为 if ip := c.request.Header.Get("X-Forwarded-For"); ip != "" { // 取第一个 IP i := strings.IndexAny(ip, ",") if i > 0 { return strings.TrimSpace(ip[:i]) } return ip } if ip := c.request.Header.Get("X-Real-IP"); ip != "" { return ip } // 从 RemoteAddr 提取 IP ra, _, _ := net.SplitHostPort(c.request.RemoteAddr) return ra}默认行为可能不安全!如果客户端可以直接访问你的应用(没有代理),他们可以伪造 X-Forwarded-For 或 X-Real-IP 标头。
自定义 IP 提取器
IPExtractor 是一个函数类型,用于从 http.Request 中提取 IP 地址:
type IPExtractor func(*http.Request) string你可以根据自己的基础架构实现自定义的 IP 提取器。
场景 1:没有代理(直连)
如果你没有放置代理(例如:面向互联网的服务),你只需要(并且必须)使用网络层的 IP 地址。任何 HTTP 标头都是不可信的,因为客户端可以完全控制要设置的标头。
s := slim.New()// 只信任 RemoteAddr,忽略所有标头s.IPExtractor = func(r *http.Request) string { ra, _, _ := net.SplitHostPort(r.RemoteAddr) return ra}场景 2:使用可信代理
如果你的应用位于一个或多个可信代理之后(如 Nginx、云负载均衡器等),你需要根据代理设置的标头来提取 IP。
使用 X-Real-IP 标头
一些代理(如 Nginx)使用 X-Real-IP 标头传递客户端 IP:
s.IPExtractor = func(r *http.Request) string { if ip := r.Header.Get("X-Real-IP"); ip != "" { return ip } // 回退到 RemoteAddr ra, _, _ := net.SplitHostPort(r.RemoteAddr) return ra}使用 X-Forwarded-For 标头
X-Forwarded-For (XFF) 是用于中继客户端 IP 地址的常用标头。在代理的每一跃点,它们都会在标头末尾附加请求 IP 地址。
以下示例关系图演示了此行为:
┌──────────┐ ┌──────────┐ ┌──────────┐───────────>│ Proxy 1 │───────────>│ Proxy 2 │───────────>│ Your app │ 客户端 (a) │ (IP: b) │ │ (IP: c) │ │ │ └──────────┘ └──────────┘ └──────────┘情况 1. 客户端直连 Proxy 1XFF: "" "a" "a, b" ~~~~~~情况 2. 客户端已通过代理 xXFF: "x" "x, a" "x, a, b" ~~~~~~~~~ ↑ 你的应用程序将看到的内容从左边提取(最不安全):
s.IPExtractor = func(r *http.Request) string { xff := r.Header.Get("X-Forwarded-For") if xff == "" { ra, _, _ := net.SplitHostPort(r.RemoteAddr) return ra } // 取第一个 IP i := strings.IndexAny(xff, ",") if i > 0 { return strings.TrimSpace(xff[:i]) } return xff}这种方式不安全!客户端可以伪造 X-Forwarded-For 标头。只有当你确定所有流量都经过可信代理时才使用。
从右边提取(推荐):
如果你知道代理的数量(例如:1 个负载均衡器),可以从右边倒数提取:
// 假设有 1 个可信代理s.IPExtractor = func(r *http.Request) string { xff := r.Header.Get("X-Forwarded-For") if xff == "" { ra, _, _ := net.SplitHostPort(r.RemoteAddr) return ra } ips := strings.Split(xff, ",") // 从右边数第 2 个(跳过最后一个代理添加的 IP) if len(ips) >= 2 { return strings.TrimSpace(ips[len(ips)-2]) } // 如果只有一个 IP,直接返回 return strings.TrimSpace(ips[0])}场景 3:云平台(AWS、GCP 等)
云平台通常有自己的标头:
// AWS 使用 X-Forwarded-Fors.IPExtractor = func(r *http.Request) string { // AWS ALB 会在 XFF 的最左边添加客户端 IP xff := r.Header.Get("X-Forwarded-For") if xff == "" { ra, _, _ := net.SplitHostPort(r.RemoteAddr) return ra } ips := strings.Split(xff, ",") // 取第一个(客户端真实 IP) return strings.TrimSpace(ips[0])}// Cloudflare 使用 CF-Connecting-IPs.IPExtractor = func(r *http.Request) string { if ip := r.Header.Get("CF-Connecting-IP"); ip != "" { return ip } ra, _, _ := net.SplitHostPort(r.RemoteAddr) return ra}完整示例
package mainimport ( "net" "net/http" "strings" "go-slim.dev/slim")func main() { s := slim.New() // 配置 IP 提取器(根据你的基础架构选择) s.IPExtractor = extractIPFromXFF s.GET("/", func(c slim.Context) error { ip := c.RealIP() return c.String(http.StatusOK, "Your IP: " + ip) }) s.Start(":1324")}// 从 X-Forwarded-For 提取 IP(假设有 1 个可信代理)func extractIPFromXFF(r *http.Request) string { xff := r.Header.Get("X-Forwarded-For") if xff == "" { ra, _, _ := net.SplitHostPort(r.RemoteAddr) return ra } ips := strings.Split(xff, ",") if len(ips) >= 2 { // 跳过最后一个(代理的 IP) return strings.TrimSpace(ips[len(ips)-2]) } return strings.TrimSpace(ips[0])}最佳实践
- 了解你的架构:明确知道有多少代理层、它们使用什么标头
- 不要盲目信任标头:如果没有代理,不要使用
X-Forwarded-For等标头 - 从右边提取:如果使用 XFF,从右边倒数提取,跳过已知的代理 IP
- 测试:在生产环境前测试 IP 提取逻辑
- 日志记录:记录提取的 IP 和原始标头,便于调试
相关 API
Context#RealIP()
func (c Context) RealIP() string返回客户端的真实 IP 地址。行为由 Slim#IPExtractor 配置。
Slim#IPExtractor
type IPExtractor func(*http.Request) stringtype Slim struct { // ... IPExtractor IPExtractor // ...}IP 提取器函数。如果为 nil,使用默认的回退行为。