c.clientip()在生产环境几乎必然失效,因其默认仅信任127.0.0.1/8和::1/128,线上经nginx/cdn后返回代理内网地址而非真实出口ip;必须显式调用settrustedproxies配置可信代理网段,否则黑白名单失效。

为什么 c.ClientIP() 在生产环境几乎必然失效
c.ClientIP() 默认只信任 127.0.0.1/8 和 ::1/128,线上请求经过 Nginx、CDN 或云负载均衡后,c.ClientIP() 返回的通常是代理内网地址(如 10.0.0.3 或 127.0.0.1),而非用户真实出口 IP。这不是 bug,而是设计使然——它不自动信任任何外部代理。
必须显式配置可信代理链,否则中间件拿到的就是错 IP,黑白名单形同虚设。
- 用
engine.SetTrustedProxies([]string{"192.168.0.0/16", "10.0.0.0/8"})告诉 Gin 哪些网段是可控代理,之后c.ClientIP()才会从X-Forwarded-For中安全取值 - 若上游是 Cloudflare 或阿里云 SLB,需把它们的官方 IP 段加入
SetTrustedProxies(例如 Cloudflare 的173.245.48.0/20) - 禁用
SetTrustedProxies(nil)或传空切片——这会让c.ClientIP()退化为c.Request.RemoteAddr,仍不可靠
如何安全提取真实客户端 IP 并校验有效性
不能无条件信任 X-Forwarded-For 头,攻击者可伪造;也不能 fallback 到 RemoteAddr 就完事——它带端口且含 IPv6 方括号(如 [2001:db8::1]:54321),直接解析会 panic。
推荐做法:优先读 X-Real-IP(由最外层可信代理设置), fallback 到 X-Forwarded-For 最左非私有 IP,最后才考虑 RemoteAddr 拆解。
-
X-Real-IP需确认 Nginx 已配proxy_set_header X-Real-IP $remote_addr; - 若用
X-Forwarded-For,先用strings.Split(c.Request.Header.Get("X-Forwarded-For"), ",")[0]取第一个,再用net.ParseIP()解析;失败则跳过 - 解析成功后,调用
ip.IsGlobalUnicast()过滤掉127.0.0.0/8、10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、fd00::/8等私有/保留地址 - 对
RemoteAddr必须先net.SplitHostPort()拆出 host 部分,再net.ParseIP()
黑白名单匹配必须用 net.IPNet.Contains,别碰字符串前缀
用 strings.HasPrefix(ipStr, "192.168.1.") 或正则匹配 CIDR,在 IPv6 下完全不可靠:"2001:db8:1::1" 和 "2001:db8::/32" 字符串前缀不同,但实际属于同一网段;而 "::ffff:192.168.1.1"(IPv4-mapped IPv6)更会直接被字符串逻辑漏掉。
标准库 net.IPNet.Contains 自动处理掩码对齐、IPv4/IPv6 格式归一化,是唯一可靠方式。
- 预加载时,对每个 CIDR 字符串调用
net.ParseCIDR()得到*net.IPNet;单 IP(如"192.168.1.100")用net.ParseIP()+net.CIDRMask(32,32)或net.CIDRMask(128,128)构造 /32 或 /128 子网 - 运行时统一用
ipnet.Contains(clientIP)判断,clientIP无需手动转To4()或To16()——Contains内部已处理 - 黑名单必须优先于白名单判断:先查是否在黑名单,命中则直接
c.AbortWithStatus(http.StatusForbidden);未命中再查白名单;都不中则拒绝
热更新黑白名单规则与 atomic.Value 的正确用法
硬编码或每次请求都重读配置文件会导致规则无法动态生效,且频繁解析 CIDR 影响性能。用 atomic.Value 包裹指针类型是 Go 官方推荐的零停机热更新方案。
注意:不能直接存 slice,因为 slice 是引用类型,赋值时底层 array 可能被并发修改;必须存指向 slice 的指针。
- 定义
var whiteList atomic.Value,初始化时whiteList.Store(&[]*net.IPNet{}) - 更新规则时,新建
[]*net.IPNet,解析全部 CIDR,再whiteList.Store(&newList) - 中间件中取值:
listPtr := whiteList.Load().(*[]*net.IPNet),然后遍历(*listPtr) - 避免在热更新期间中间件拿到 nil 或部分写入的 slice ——
atomic.Value保证 Load/Store 原子性,但新旧 slice 内容互不影响
万级规则以上才需考虑 trie 或 bitmap 优化;日常几百条,线性扫描 *net.IPNet 切片足够快,别过早优化。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











