根本原因是nginx未透传真实ip,req.remoteaddr返回的是直连nginx的地址;需配置proxy_set_header x-real-ip $remote_addr和x-forwarded-for $proxy_add_x_forwarded_for,并在go中安全解析x-forwarded-for首段合法公网ip。

Go 服务在 Nginx 反向代理后拿不到真实 IP,根本原因不是 Go 写得不对,而是 Nginx 没把 IP 传进来,或者 Go 没从对的 header 里取。
为什么 req.RemoteAddr 返回的是 Nginx 的地址?
Nginx 默认做 TCP 层转发时会改写原始 IP 包的源地址(尤其在非透明代理模式下),req.RemoteAddr 读的是底层 socket 的 RemoteAddr(),拿到的只能是直连它的上一跳——也就是 Nginx 本机或其回环地址(如 127.0.0.1:54321)。这不是 Go 的限制,是网络栈事实。
必须依赖 HTTP header 显式传递真实 IP,且 Nginx 配置和 Go 解析要匹配。
X-Real-IP 和 X-Forwarded-For 该用哪个?
两者用途不同,不能混用:
-
X-Real-IP是单值 header,Nginx 用$remote_addr填充,只反映“直接连接 Nginx 的客户端”IP。适用于单层代理(比如用户 → Nginx → Go);如果 Nginx 前还有 CDN 或 LB,这里可能已经是中间节点 IP。 -
X-Forwarded-For是逗号分隔的 IP 链,格式为client_ip, proxy1_ip, proxy2_ip。Nginx 用$proxy_add_x_forwarded_for追加自身 IP,所以 Go 必须取第一个非私有、非保留的 IP —— 不能直接strings.Split(..., ",")[0]就完事。
推荐优先信任 X-Forwarded-For,但必须校验 IP 合法性;X-Real-IP 可作为 fallback,前提是确认代理链只有一层。
Go 中安全提取真实 IP 的代码怎么写?
别信网上抄来的“取第一个逗号前”的简单逻辑。攻击者可以伪造 X-Forwarded-For: 1.1.1.1, 127.0.0.1 绕过风控。正确做法:
- 明确信任哪些代理 IP(比如你自己的 Nginx 内网地址:
10.0.0.0/8、192.168.0.0/16) - 从
X-Forwarded-For倒着遍历,跳过所有可信代理 IP,取第一个不可信的公网 IP - 同时检查
X-Real-IP是否合法,可与前者交叉验证
示例片段:
func getClientIP(req *http.Request) string {
// 先看 X-Real-IP(单值,较干净)
if ip := req.Header.Get("X-Real-IP"); ip != "" && net.ParseIP(ip) != nil {
return ip
}
// 再解析 X-Forwarded-For(多值,需清洗)
if xff := req.Header.Get("X-Forwarded-For"); xff != "" {
for _, ip := range strings.Split(xff, ",") {
ip = strings.TrimSpace(ip)
if ip == "" || !isValidPublicIP(ip) {
continue
}
return ip
}
}
// 最终 fallback 到 RemoteAddr(仅限调试或内网直连)
host, _, _ := net.SplitHostPort(req.RemoteAddr)
if net.ParseIP(host) != nil {
return host
}
return "0.0.0.0"
}
func isValidPublicIP(ipStr string) bool {
ip := net.ParseIP(ipStr)
if ip == nil {
return false
}
return !ip.IsPrivate() && !ip.IsLoopback() && !ip.IsUnspecified()
}
容易被忽略的 Nginx 配置陷阱
即使 Go 代码写对了,Nginx 配置错一步,header 就传不进来:
- 必须在
location块里写proxy_set_header,写在server或http块里可能不生效(尤其当有多个location且部分没覆盖时) -
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for才正确;写成$remote_addr会丢掉原始链路 - 如果 Nginx 前还有云厂商 LB(如 AWS ALB、阿里云 SLB),它们通常也加
X-Forwarded-For,此时 Nginx 的$proxy_add_x_forwarded_for会把 LB 的 IP 也追加进去,导致 header 更长——你的 Go 解析逻辑必须能处理多层 - 确保 Nginx 没开启
underscores_in_headers on以外的 header 过滤(某些旧版默认丢弃带下划线的 header)
真实环境里,IP 链可能长达 4–5 段,而你真正能信的只有最左端那个非内网 IP —— 其余都可能是被污染的。校验逻辑比看起来重得多。











