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 1
XFF: "" "a" "a, b"
~~~~~~
情况 2. 客户端已通过代理 x
XFF: "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-For
s.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-IP
s.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 main
import (
"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) string
type Slim struct {
// ...
IPExtractor IPExtractor
// ...
}
IP 提取器函数。如果为 nil,使用默认的回退行为。