应在Gin中间件中通过SetTrustedProxies配置可信代理,用net.ParseIP解析c.ClientIP()获取真实IP,再以预解析的*net.IPNet切片调用Contains进行CIDR匹配,黑名单优先、豁免探针路径,并务必c.Abort()终止请求。

如何在Gin中间件中解析并匹配IP段
直接用 c.ClientIP() 获取的 IP 可能是代理后的真实客户端 IP,但默认不启用可信代理头解析,容易误判为 127.0.0.1 或内网地址。必须先配置 gin.SetMode(gin.ReleaseMode) 并显式设置可信代理列表,否则 c.ClientIP() 会返回不可靠结果。
IP段匹配不能靠字符串前缀(如 "192.168.1."),必须转为整数进行 CIDR 判断。推荐用标准库 net.ParseIP + net.IPNet.Contains,避免手动拆分和掩码计算出错。
- 可信代理需提前设置:
router.TrustedPlatform = gin.PlatformCloudflare或自定义gin.ProxyHeaders - 获取真实IP后,统一转为
net.IP类型再比对 - 白名单/黑名单规则建议存于内存 map 或 Redis,避免每次读配置文件
用 net.IPNet 实现 CIDR 段匹配的中间件
Go 标准库的 net.IPNet 天然支持 CIDR 匹配(如 "192.168.0.0/16"),比正则或字符串截取更准确、性能更好。错误做法是把 IP 转成点分十进制字符串再做 strings.HasPrefix,这无法处理 /24 和 /25 等不同掩码长度。
示例中间件逻辑:
func IPBlockMiddleware(blacklist []*net.IPNet) gin.HandlerFunc {
return func(c *gin.Context) {
ip := net.ParseIP(c.ClientIP())
if ip == nil {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "invalid ip"})
return
}
for _, cidr := range blacklist {
if cidr.Contains(ip) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "ip blocked"})
return
}
}
c.Next()
}
}
- 初始化时预解析所有 CIDR 字符串为
*net.IPNet,避免每次请求都调用net.ParseCIDR - 注意 IPv6 地址需用
net.ParseIP正确识别,不要硬编码 IPv4 判断 - 若黑名单条目超过千条,建议改用
iprange库做区间合并优化查询
为什么不能只靠 Nginx 做 IP 段拦截
Nginx 的 allow/deny 确实快,但它在七层之前就丢弃请求,导致 Gin 完全收不到日志、无法联动限流或人机验证。真实攻击往往伴随 UA 异常、请求头缺失、高频低质量 payload,单靠 IP 段过滤会漏掉大量伪装成合法 IP 的 BOT 流量。
- Nginx 层适合封禁已确认的恶意 AS 段(如某 IDC 全部出口 IP)
- Gin 中间件更适合做动态决策:结合请求路径、Header、频率特征联合判断
- 两者应分层协作——Nginx 做粗筛,Gin 做细粒度策略(如
/api/login下对某 IP 段+异常 UA 组合直接拦截)
常见 CIDR 配置陷阱与调试方法
开发时容易把 "10.0.0.0/8" 写成 "10.0.0.0/24",或者误以为 "192.168.1.100" 属于 "192.168.1.0/24" 就一定匹配成功——其实取决于 net.ParseCIDR 解析是否成功。调试阶段务必打印解析后的 IPNet.Mask 和 IP.String() 对比。
- 用
fmt.Printf("mask: %v, ip: %v\n", cidr.Mask, ip)验证匹配逻辑 - 测试用例必须覆盖 IPv4 / IPv6、/32 单 IP、/0 默认路由等边界情况
- 线上环境禁止用
log.Println打印每个请求 IP,改用结构化日志 + 抽样采样
真正难的是把 IP 段拦截和行为分析联动起来——比如同一个 /24 段里,只有连续 5 分钟内触发 100 次 /api/search 的才进黑名单,而不是一刀切封整个段。这需要状态缓存和滑动窗口,不是单纯 CIDR 匹配能解决的。


















