Go服务中r.RemoteAddr返回的是Nginx代理IP而非用户真实IP;X-Real-IP更可靠但需Nginx正确配置proxy_set_header X-Real-IP $remote_addr;X-Forwarded-For易被伪造,应取首段非私有IP并校验合法性,最后才fallback到RemoteAddr。

Go 服务里直接读 r.RemoteAddr 拿到的是 Nginx 的 IP,不是用户真实 IP
这是最常踩的第一个坑:Go 标准库的 http.Request.RemoteAddr 返回的是 TCP 连接对端地址,也就是 Nginx 所在机器的 IP(比如 127.0.0.1:54321),跟浏览器发起请求的用户完全无关。哪怕你用的是 net/http 原生写法,不依赖任何框架,这个行为也一样。
X-Real-IP 和 X-Forwarded-For 哪个更可靠?
优先用 X-Real-IP,但必须确保 Nginx 配置了 proxy_set_header X-Real-IP $remote_addr。它只存一个值,简单明确,适合单层代理场景。
X-Forwarded-For 是链式结构(如 203.0.113.5, 192.168.10.1),第一个 IP 是客户端原始地址,后面是各级代理 IP。但它的风险在于:X-Forwarded-For 可被客户端伪造,如果 Nginx 没做信任校验,后端直接取第一个字段就可能被绕过。
所以实际代码中建议:
- 先检查
X-Real-IP,非空且格式合法就用它 - 再 fallback 到
X-Forwarded-For的第一个非私有 IP(跳过127.0.0.1、10.0.0.0/8、172.16.0.0/12、192.168.0.0/16等) - 最后才退化到
r.RemoteAddr(仅作兜底,生产环境不应触发)
Nginx 配置漏掉 $proxy_add_x_forwarded_for 会导致链路断裂
很多人写成 proxy_set_header X-Forwarded-For $remote_addr,这是错的。它会覆盖上游已有的 X-Forwarded-For,把整条链路“截断”——后端看到的永远只有最新一跳的 IP,丢失了原始客户端信息。
正确写法必须是:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
$proxy_add_x_forwarded_for 的行为是:
— 如果上游请求已有 X-Forwarded-For,就在末尾追加当前 Nginx 的 $remote_addr;
— 如果没有,就设为 $remote_addr。
这个变量是 Nginx 内置逻辑,不能手拼。
Go 里解析 IP 要做合法性校验,别信 Header 里的任意字符串
Header 值是纯字符串,攻击者可以发 X-Real-IP: 127.0.0.1<script>...</script> 或 X-Forwarded-For: 8.8.8.8, 1.2.3.4, 999.999.999.999。直接 net.ParseIP() 可能返回 nil,但没校验就入库或限流会出问题。
推荐做法:
- 用
net.ParseIP()解析后,再调用ip.To4() != nil || ip.To16() != nil确保是有效 IPv4/IPv6 - 如果是 IPv4,额外排除私有网段(
ip.IsPrivate()在 Go 1.22+ 支持,老版本需手动判断) - 避免用
strings.Split()处理X-Forwarded-For后直接取首段——要遍历每个分段并逐个校验
真正复杂的地方不在怎么写,而在你是否清楚自己处在几层代理之后。如果前面还有 CDN 或云 LB,$remote_addr 本身可能已经是上一级代理的 IP,这时光靠 Nginx 配置不够,得配合 real_ip_header + set_real_ip_from 模块做可信源识别。这点很容易被忽略。


















