Gin中可通过路由中间件调用c.RemoteIP()获取真实客户端IP,判断其是否在预解析的CIDR黑名单中,匹配则直接c.AbortWithStatus(403)终止请求;需确保中间件注册在所有路由前、静态文件路由显式启用、反向代理配置正确,并用sync.RWMutex保障黑名单热更新并发安全。

如何在Gin中拦截指定IP并返回403
直接在路由中间件里判断 RemoteIP() 是否在黑名单中,匹配就调用 c.AbortWithStatus(403)。别用 c.String() 或 c.JSON() 后再 return,那样响应已发,AbortWithStatus 无效。
常见错误是把IP提取逻辑写在 handler 里而不是中间件——这样请求体可能已被读取(比如用了 c.ShouldBindJSON()),再 abort 就晚了。
- 用
c.ClientIP()要小心:它会解析X-Forwarded-For,如果上游没做可信代理配置,容易被伪造;生产环境建议用c.RemoteIP()(即 TCP 连接真实来源) - 黑名单存内存用
map[string]struct{},查 O(1);别用切片遍历,1000 个 IP 就明显卡顿 - 若需动态更新,用
sync.RWMutex包裹 map,读多写少时性能更稳
IP黑名单怎么加载和热更新
启动时从文件或环境变量加载初始名单没问题,但线上封禁新IP不能重启服务。最轻量的做法是提供一个 POST 接口,接收 JSON 数组如 ["192.168.1.100", "2001:db8::1"],追加进全局 map。
注意并发安全:写操作加 mutex.Lock(),读操作用 mutex.RLock();同时避免在写期间阻塞大量请求,可考虑用原子替换(如 atomic.StorePointer 指向新 map)。
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 文件路径建议用
./config/blocked_ips.txt,每行一个 CIDR 或单 IP(如192.168.1.0/24),解析时用net.ParseCIDR()判断是否包含 - 不要硬编码黑名单到代码里——改一次就得发版
- 热更新后建议打日志:
log.Printf("blocked %d IPs, total: %d", len(newList), len(globalBlockMap))
Gin中间件里怎么判断CIDR网段
标准库 net 包的 *net.IPNet.Contains() 是唯一可靠方式。别用字符串前缀匹配,IPv6 和掩码边界很容易出错。
示例:检查 192.168.5.7 是否在 192.168.0.0/16 内:
ip := net.ParseIP("192.168.5.7")
_, ipnet, _ := net.ParseCIDR("192.168.0.0/16")
if ipnet.Contains(ip) {
c.AbortWithStatus(403)
}
- 每次请求都
ParseCIDR开销大,应预解析好存 map 或 slice,运行时只调Contains() - IPv6 地址要转成 IPv6 格式再比(
ip.To16()),否则Contains()可能返回 false - 注意
0.0.0.0/0这种全匹配规则——测试时容易误配导致全站 403
为什么用了中间件还是拦不住某些请求
典型原因是中间件注册顺序错了。Gin 的中间件是链式执行,必须在所有路由注册前全局 use,或者至少在目标路由组上调用 Use()。如果只对某个 GET /api 加了中间件,而攻击者直打 /static/xxx.js 就绕过了。
- 静态文件路由(
router.Static())默认不走中间件,除非显式调用Use()——这是最多人踩的坑 - 健康检查接口(如
/healthz)常被忽略,也得套同一套 IP 检查逻辑 - 如果用了反向代理(Nginx),确认
real_ip_header和set_real_ip_from配置正确,否则c.ClientIP()取到的是代理IP而非真实IP
复杂点在于 CIDR 解析和并发更新的组合——稍不注意就会出现“刚加进去的 IP 还没生效”或者“查着查着 panic”,得靠单元测试覆盖 Contains 边界 case 和锁逻辑。

















