c.ClientIP()在线上环境几乎必然返回127.0.0.1或内网地址,因其默认仅信任127.0.0.1/8和::1/128等本地段,而真实请求必经Nginx/CDN等未被信任的代理,导致fallback至RemoteAddr(如10.0.0.3)。

为什么 c.ClientIP() 在线上环境几乎必然返回 127.0.0.1 或内网地址
因为 Gin 默认只信任 127.0.0.1 和 ::1,一旦前面有 Nginx、CDN 或 Traefik,c.ClientIP() 就会退化为代理的 RemoteAddr(比如 10.0.0.3),而非用户真实出口 IP。这不是 bug,是设计使然——它不自动信任任何外部代理。
正确做法是显式声明可信代理网段,并配合请求头解析:
- 若只有一层代理(如 Nginx 直连 Go),确保 Nginx 配置了
proxy_set_header X-Real-IP $remote_addr;,然后在 Gin 中用c.Request.Header.Get("X-Real-IP") - 若有多层(如 CDN → Nginx → Go),必须调用
engine.SetTrustedProxies([]string{"192.168.0.0/16", "203.208.60.0/24"}),再调用c.ClientIP()——此时它才真正可靠 - 永远不要直接信任客户端传来的
X-Forwarded-For全字段;SetTrustedProxies的作用就是让 Gin 自动剔除链中可信节点的 IP,只保留最外层不可信来源
怎么用 netip.Prefix + atomic.Value 实现支持 CIDR 的热更新黑名单
用 map[string]bool 存单个 IP 看似简单,但无法匹配网段(如 192.168.1.0/24),并发读写要加锁,更致命的是规则更新时直接替换 map,正在处理的请求可能看到新旧规则混杂状态。
推荐方案是封装 netip.Prefix 切片 + atomic.Value:
- 初始化时构建
[]netip.Prefix,按掩码长度倒序排列(保证/32优先于/24) - 用
atomic.Value包装该切片,更新时Store整个新切片——无锁、原子、无中间态 - 查询时遍历切片,对每个
prefix.Contains(ip)做判断,命中即拦截 - 白名单逻辑同理,但建议“先查黑名单,再查白名单”,并给白名单更高优先级(例如白名单匹配后直接放行)
拦截后不加 c.Abort() 和 return 会导致什么后果
常见错误是写了 c.JSON(403, gin.H{"msg": "forbidden"}) 却没跟 c.Abort() 和 return,导致后续中间件甚至业务 handler 继续执行——数据库可能已被改、日志已落盘、通知已发出。
c.Abort() 只阻止后续中间件执行,不终止当前函数体;漏掉 return,c.Next() 仍会被调用,请求继续向下流转。
正确写法示例:
if isBlocked(ip) {
c.Writer.WriteHeader(403)
c.Writer.Write([]byte("Forbidden"))
c.Abort()
return
}
这样 Gin 的 Logger 中间件不会记录该请求(因未进入后续链路),也不会触发 Recovery 捕获,避免污染监控指标。
哪些路径必须豁免黑白名单校验
健康检查和静态资源路径也得走同一套过滤逻辑,否则扫描器可通过加载 JS 文件试探接口路径,或监控系统因被拦截而误报服务异常。
- 必须豁免的路径包括:
/healthz、/metrics、/readyz等探针路径 - 静态资源路径(如
/public/js/app.js)不能跳过过滤——它们同样暴露服务结构,常被扫描器利用 - 豁免逻辑应放在黑白名单中间件内部,用
c.Request.URL.Path匹配,而不是靠注册顺序绕过 - 注意:豁免仅指“不校验 IP”,不代表不鉴权或不限流;敏感探针(如
/debug/pprof)仍需额外保护
proxy_set_header X-Real-IP $remote_addr;,或者 Gin 忘记调用 SetTrustedProxies,整个黑白名单就形同虚设——攻击者只需伪造头即可绕过。


















