Go服务需校验Nginx可信代理IP后,优先取X-Real-IP头值,否则解析X-Forwarded-For最左非私有IP,不可直接信任RemoteAddr或未校验的请求头。
go 服务作为后端被 nginx 反向代理时,$remote_addr 拿到的永远是 nginx 的 ip,不是用户真实 ip。必须依赖 nginx 显式传递的 http 头(如 x-real-ip 或 x-forwarded-for),且 go 侧要主动解析、校验、信任——否则直接读头就是裸奔。
为什么 r.RemoteAddr 不可信
Go 的 http.Request.RemoteAddr 是 TCP 连接对端地址,即 Nginx 所在机器的出口 IP(比如 10.0.1.5:42891)。它不感知 HTTP 层头,也不做任何代理链还原。即使你在 Nginx 配了 proxy_set_header X-Real-IP $remote_addr,Go 也不会自动切换来源。
常见错误现象:
- 所有请求日志里 IP 都是同一个内网地址(如 192.168.0.10)
- 基于 IP 的限流/封禁完全失效
- 地理位置识别全错成机房所在地
根本原因:没从 HTTP 头取值,也没做可信源过滤。
X-Real-IP vs X-Forwarded-For 怎么选
X-Real-IP 是单值头,Nginx 用 $remote_addr 覆盖写入,语义明确——“这是我认为的真实客户端 IP”。X-Forwarded-For 是逗号分隔列表,格式为 client, proxy1, proxy2,最左是原始客户端 IP,但可被任意伪造。
实操建议:
- ✅ 优先信任 X-Real-IP,前提是 Nginx 配置中只由你可控的代理写入(比如你自己的 Nginx,不是 CDN 或第三方 LB)
- ⚠️ 若必须用 X-Forwarded-For(例如前端有多层不可控代理),必须配合可信代理网段白名单 + 递归解析逻辑
- ❌ 绝对不要直接用 r.Header.Get("X-Forwarded-For") 拆第一个 IP 就完事——攻击者 curl -H "X-Forwarded-For: 1.2.3.4" 就能绕过
Go 代码里安全提取真实 IP 的写法
核心逻辑:只接受来自可信代理 IP(如 Nginx 内网地址)发来的请求,再从中取头。不能无条件信任所有请求头。
示例(使用标准库,不依赖第三方):
func getClientIP(r *http.Request, trustedProxies []string) string {
// 1. 先检查连接来源是否在可信代理列表中
clientIP, _, _ := net.SplitHostPort(r.RemoteAddr)
isTrusted := false
for _, ipNet := range trustedProxies {
_, subnet, _ := net.ParseCIDR(ipNet)
if subnet.Contains(net.ParseIP(clientIP)) {
isTrusted = true
break
}
}
if !isTrusted {
return clientIP // 不可信来源,退回到 RemoteAddr(至少是直连 IP)
}
<pre class="brush:php;toolbar:false;">// 2. 从可信来源的请求头中取值
if ip := r.Header.Get("X-Real-IP"); ip != "" {
return ip
}
if ip := r.Header.Get("X-Forwarded-For"); ip != "" {
// 取最左非空 IP(跳过可能的空格或伪造项)
for _, s := range strings.Split(ip, ",") {
if realIP := strings.TrimSpace(s); realIP != "" {
return realIP
}
}
}
return clientIP}
调用时传入你的 Nginx 内网网段,比如:trustedProxies := []string{"10.0.0.0/16", "192.168.1.0/24"}
注意点:
- trustedProxies 必须和 Nginx 的 set_real_ip_from 配置严格一致
- 如果用了云 WAF 或 CDN,它们可能用 X-Forwarded-For 或 X-Cluster-Client-Ip,需按实际头名调整 r.Header.Get()
- net.ParseIP 对 IPv6 地址返回 nil,若需支持 IPv6,应先用 net.ParseIP 判断再走 net.ParseCIDR
Nginx 配置必须同步启用 realip 模块
光在 proxy_set_header 里加头没用——Go 侧信任的是“谁发来的这个头”,而不是“头里写了啥”。Nginx 必须用 ngx_http_realip_module 告诉自己:“这些 IP 是我信得过的代理,它们传来的 X-Forwarded-For 我才认”。
关键配置项(放在 http 或 server 块):set_real_ip_from 10.0.0.0/16;set_real_ip_from 192.168.1.0/24;real_ip_header X-Forwarded-For;real_ip_recursive on;
为什么这步不能省:
- real_ip_recursive on 表示当 X-Forwarded-For 有多个 IP 时,从右往左剔除已知代理 IP,剩下一个才是客户端 IP
- 如果没开,Nginx 自身的 $realip_remote_addr 变量就拿不到真实值,而某些日志或限流模块依赖它
- 更重要的是:它让 Nginx 在内部完成一次可信校验,和 Go 侧的校验形成双重保险
容易踩的坑:
- 把公网 IP 段(如 0.0.0.0/0)写进 set_real_ip_from ——等于完全放弃校验
- 忘记 reload Nginx 配置,改了 real_ip_header 却没生效
- Go 服务监听在 127.0.0.1,而 Nginx proxy_pass 也指向 127.0.0.1,导致 r.RemoteAddr 是 127.0.0.1:xxxx,无法匹配任何网段白名单
最常被忽略的一点:真实环境里,Nginx 往往不是唯一入口。WAF、CDN、K8s Ingress、甚至另一层 Nginx 都可能插在中间。每多一层,X-Forwarded-For 就多一个 IP,白名单就得覆盖全部代理网段,而 X-Real-IP 只有最后一层能写——所以务必确认你控制的那层 Nginx 是最终可信出口,且它的 IP 确实落在你写的 set_real_ip_from 范围内。


















