应调用c.ClientIP()前先用r.SetTrustedProxies()配置可信代理列表,否则默认仅信任localhost,反向代理后易获取错误IP;黑名单宜用atomic.Value存map[string]struct{}实现无锁读与原子更新,拦截时须用c.AbortWithStatusJSON统一返回格式,避免c.Abort()后无响应。

中间件里怎么拿到客户端真实 IP
直接用 c.ClientIP() 在反向代理后大概率拿错——它默认读 X-Forwarded-For,但若 Nginx 没配或配错,会返回负载均衡器或网关的 IP,导致黑名单失效。
正确做法是先配置 Gin 的信任代理列表,再调用 c.ClientIP():
r := gin.New()
r.SetTrustedProxies([]string{"10.0.0.0/8", "192.168.0.0/16"}) // 根据实际内网段调整
- 必须显式调用
SetTrustedProxies,Gin 默认只信任 localhost,不设就永远拿不到真实 IP - 传入的 CIDR 列表要覆盖所有前置代理(Nginx、ALB、Cloudflare 等),漏一个就可能被伪造
X-Forwarded-For - 如果用的是 Cloudflare,还需额外启用
Engine.AppEngine或手动读CF-Connecting-IP头
黑名单数据存在哪才够快又可热更新
用全局 map + sync.RWMutex 是常见误区:写操作(增删 IP)会阻塞所有读,QPS 上千时中间件延迟明显上升。
推荐方案是用 map[string]struct{} 配合原子替换:
立即学习“go语言免费学习笔记(深入)”;
var blackList atomic.Value // 存 *map[string]struct{}
// 初始化
blackList.Store(&map[string]struct{}{"192.168.1.100": {}})
// 更新时创建新 map,原子替换
newMap := make(map[string]struct{})
for k := range *oldMap {
if k != ipToRemove {
newMap[k] = struct{}{}
}
}
blackList.Store(&newMap)
- 查 IP 时直接
_, ok := (*blackList.Load().*map[string]struct{})[ip],无锁读,性能接近原生 map - 避免用 Redis 或数据库查黑名单——每次请求都走网络 IO,中间件耗时从微秒级变成毫秒级
- 如果黑名单超 10 万条,考虑改用
golang.org/x/exp/slices+ 排序二分,但多数场景 map 足够
中间件里拦截后怎么统一返回格式
Gin 的 c.Abort() 不等于响应结束——如果你只 Abort 没写响应体,客户端会卡住或收到空响应。
必须显式调用 c.JSON 或 c.Status 并终止流程:
func BlacklistMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
if _, blocked := (*blackList.Load().*map[string]struct{})[ip]; blocked {
c.AbortWithStatusJSON(403, gin.H{"error": "IP blocked"})
return
}
c.Next()
}
}
- 用
c.AbortWithStatusJSON而不是c.JSON+c.Abort(),少写一行且语义明确 - 别返回 429(Too Many Requests)——那是限流用的,黑名单该用 403,否则前端监控容易误判
- 如果项目已有统一错误响应结构(比如带 code/msg/data 字段),这里必须复用,不能另起一套
为什么 reload 黑名单后旧连接还在跑
中间件注册后就固化在路由树里,blackList.Store() 只影响后续请求,已进入中间件链但尚未执行到你的逻辑的请求,仍按旧黑名单判断。
- 这是 Go HTTP 中间件模型的天然限制,不是 bug,无法“实时中断”正在处理的请求
- 若需秒级生效,只能配合优雅关闭(
srv.Shutdown())+ 进程重启,或者让前端重试(加 retry-after 头) - 真正要注意的是:别在中间件里做耗时操作(如查 DB、调外部 API),否则 reload 后堆积的请求会拖慢整个服务
动态黑名单本质是查表动作,重点在查得快、换得稳、别让一次 reload 引发雪崩——其余都是细节。


















