c.ClientIP()不可直接使用,因其默认仅信任127.0.0.1和::1,线上经Nginx或CDN后返回代理内网地址而非真实IP;必须配置可信代理段并解析X-Real-IP或X-Forwarded-For头,否则易被伪造绕过。

为什么 c.ClientIP() 不能直接用
因为默认情况下它只信任 127.0.0.1 和 ::1,线上走 Nginx 或 CDN 后,c.ClientIP() 返回的往往是代理内网地址(比如 10.0.0.3),不是真实客户端 IP。直接拿这个值做黑白名单,等于拦错了人。
必须显式配置可信代理段,否则攻击者伪造 X-Forwarded-For 就能绕过:
- 若只有一层 Nginx,且它设置了
proxy_set_header X-Real-IP $remote_addr;,用c.Request.Header.Get("X-Real-IP")更稳妥 - 若有多层(CDN → Nginx → Gin),要用
X-Forwarded-For并取最左非信任地址,同时调用engine.SetTrustedProxies([]string{"192.168.0.0/16", "203.208.60.0/24"}) - 别依赖
c.RemoteIP(),它和c.ClientIP()底层逻辑一样,同样受信任代理配置影响
怎么写一个支持 CIDR 的 IP 白名单中间件
硬编码 map[string]bool 只能匹配单 IP,没法处理 192.168.5.0/24 这类网段;用 net.ParseCIDR + ipNet.Contains() 才能正确判断。
示例核心逻辑:
func IPWhitelist(ips []string) gin.HandlerFunc {
var nets []*net.IPNet
for _, ip := range ips {
if _, net, err := net.ParseCIDR(ip); err == nil {
nets = append(nets, net)
} else if ipAddr := net.ParseIP(ip); ipAddr != nil {
mask := net.CIDRMask(32, 32)
if ipAddr.To4() == nil {
mask = net.CIDRMask(128, 128)
}
nets = append(nets, &net.IPNet{IP: ipAddr, Mask: mask})
}
}
return func(c *gin.Context) {
realIP := c.ClientIP()
allowed := false
for _, net := range nets {
if net.Contains(net.ParseIP(realIP)) {
allowed = true
break
}
}
if !allowed {
c.AbortWithStatus(http.StatusForbidden)
return
}
c.Next()
}
}
使用时:r.Use(IPWhitelist([]string{"192.168.10.0/24", "2001:db8::/32"}))
黑名单 vs 白名单:中间件里该先查哪个
顺序直接影响安全水位。推荐「先查黑名单,再查白名单」,理由很实际:
- 黑名单通常是临时封禁(如 fail2ban 自动拉黑),需要最高优先级生效,避免被白名单覆盖
- 白名单是长期可信源,范围小、变更少,放后面查更省 CPU
- 如果某 IP 同时在黑白名单里(比如运维误配),黑名单应胜出——宁可错杀,不可漏放
不要写成两个独立中间件串行调用,容易因 c.Next() 导致逻辑割裂;合并进一个中间件,按「deny → allow → default deny」流程判断。
热更新规则时最容易崩的点
从文件或 DB 加载新规则后,直接替换全局变量会导致正在执行的请求看到新旧混合状态——比如一半 goroutine 查旧 map,一半查新 map,原子性彻底失效。
必须用 atomic.Value 包裹规则结构体,并确保每次更新都用 Store() 替换整个指针:
var rules atomic.Value
// 初始化
rules.Store(&IPRules{Whitelist: [...], Blacklist: [...]})
// 更新时
rules.Store(newRules)
// 中间件中读取
current := rules.Load().(*IPRules)
别用 sync.RWMutex 锁 map——高并发下读锁竞争反而拖慢性能;atomic.Value 是零拷贝读,更适合这种「读多写少」场景。
真正难的不是写对第一版,而是当规则从 10 条涨到 500 条、从手动维护变成自动同步时,结构没塌、性能没掉、更新不丢请求——这些细节,往往在压测后才暴露。


















