net.ParseIP 不能直接比对 CIDR 段,因其仅解析单个 IP 地址(net.IP),而 CIDR(如 "192.168.1.0/24")表示网段范围,须用 net.ParseCIDR 解析为 *net.IPNet 后调用 Contains() 判断归属。

为什么 net.ParseIP 不能直接比对 CIDR 段?
很多人写黑白名单时,直接用 net.ParseIP 解析客户端 IP,再拿它跟配置的 "192.168.1.0/24" 字符串做字符串比较——这当然永远不匹配。CIDR 不是普通字符串,它代表一个网段范围,必须用 net.IPNet 来解析和判断包含关系。
正确做法是:先用 net.ParseCIDR 把配置项转成 *net.IPNet,再用它的 Contains 方法检查请求 IP 是否落在该网段内。
-
net.ParseIP("192.168.1.5")→ 得到单个 IP 地址(net.IP) -
net.ParseCIDR("192.168.1.0/24")→ 得到网段对象(*net.IPNet),含掩码和网络地址 - 调用
ipNet.Contains(clientIP)才能真正判断是否属于该子网
如何从 c.ClientIP() 获取真实客户端 IP?
Gin 的 c.ClientIP() 默认只看 X-Forwarded-For 和 X-Real-IP,但实际部署中常经过多层反向代理(Nginx → LB → Gin),如果没配置信任代理列表,它会返回最外层代理的 IP,导致黑白名单失效。
必须显式设置可信代理 CIDR 列表,并启用 SetTrustedProxies:
router := gin.Default()
// 必须在任何路由注册前调用
router.SetTrustedProxies([]string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.1/32"})
- 不设或设空切片 →
ClientIP()只读请求头,不可信 - 设错(如漏掉 LB 的内网段)→ 返回代理 IP 而非用户真实 IP
- 若用云服务(如阿里云 SLB、AWS ALB),需查文档确认其源 IP 透传方式,有些需额外开启 X-Forwarded-For 插件
黑白名单逻辑冲突时谁优先?
常见错误是把白名单和黑名单写成两个独立循环,最后靠 return 顺序决定结果——这样容易漏掉“既在白名单又在黑名单”的边界情况,或者误判未命中任一列表的请求。
推荐统一结构:先检查白名单(允许通行),再检查黑名单(明确拒绝),其余放行。逻辑必须严格分层:
if len(whitelist) > 0 {
allowed := false
for _, ipNet := range whitelist {
if ipNet.Contains(clientIP) {
allowed = true
break
}
}
if !allowed {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "IP not in whitelist"})
return
}
}
// 白名单通过后,再查黑名单
for _, ipNet := range blacklist {
if ipNet.Contains(clientIP) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "IP in blacklist"})
return
}
}
- 白名单存在时,不在白名单的请求直接拦截,不进黑名单逻辑
- 白名单为空,则跳过白名单检查,仅走黑名单
- 两个名单都为空 → 中间件不生效,所有请求放行(这是合理默认行为)
性能敏感场景下怎么避免重复解析 CIDR?
每次请求都调用 net.ParseCIDR 是浪费——配置项通常是静态的,应该在启动时一次性解析并缓存为 []*net.IPNet,中间件里只做 Contains 判断。
别在中间件函数体内解析字符串配置,而应在初始化阶段完成:
var (
whitelistIPNets = parseCIDRs([]string{"10.0.0.0/8", "127.0.0.1/32"})
blacklistIPNets = parseCIDRs([]string{"2.2.2.2/32", "3.3.0.0/16"})
)
func parseCIDRs(cidrs []string) []*net.IPNet {
var nets []*net.IPNet
for _, cidr := range cidrs {
if ipNet, err := net.ParseCIDR(cidr); err == nil {
nets = append(nets, ipNet)
}
}
return nets
}
- 如果配置来自环境变量或配置文件,确保解析失败时有日志报警,而不是静默忽略
- IPv6 地址(如
"2001:db8::/32")同样适用net.ParseCIDR,无需特殊分支 - 注意
net.IP是切片类型,比较时不要用==,始终用Contains或Equal


















