c.ClientIP()不能直接用于内网判断,因其默认仅信任127.0.0.1,多级代理下返回的是最后一跳代理内网IP(如10.0.0.3),而非真实客户端IP;必须配合SetTrustedProxies配置可信网段,并优先从X-Real-IP或X-Forwarded-For头提取IP后,再用netip解析并比对私有地址段。

为什么 c.ClientIP() 不能直接用于内网判断
c.ClientIP() 在生产环境基本不可信,它默认只信任 127.0.0.1,而真实请求经过 Nginx、CDN 或服务网格后,返回的往往是代理内网地址(如 10.0.0.3 或 192.168.1.100),不是客户端真实出口 IP。拿它做 net.ParseIP().IsPrivate() 判断,等于用错误输入跑正确逻辑——结果全错。
必须明确两点:
- 可信代理链需显式配置:
engine.SetTrustedProxies([]string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}) - 真实客户端 IP 应从请求头提取:
c.Request.Header.Get("X-Real-IP")(单层代理)或c.Request.Header.Get("X-Forwarded-For")(多层,取最左非可信地址)
如何在 Gin 中写一个可靠的内网访问中间件
核心是:先取真实 IP,再判断是否属于私有地址段。别用 net.ParseIP().IsPrivate() 直接判——它对 IPv6 和某些边缘格式支持不稳;推荐用 netip.ParseAddr() + netip.Prefix.Contains()。
示例中间件:
func InternalOnly() gin.HandlerFunc {
return func(c *gin.Context) {
ipStr := c.Request.Header.Get("X-Real-IP")
if ipStr == "" {
ipStr = c.ClientIP() // fallback,仅用于开发环境直连
}
ip, err := netip.ParseAddr(ipStr)
if err != nil {
c.AbortWithStatusJSON(403, gin.H{"error": "invalid ip"})
return
}
// 定义常见内网网段(含 IPv6 ULA)
internalRanges := []netip.Prefix{
netip.MustParsePrefix("10.0.0.0/8"),
netip.MustParsePrefix("172.16.0.0/12"),
netip.MustParsePrefix("192.168.0.0/16"),
netip.MustParsePrefix("100.64.0.0/10"), // CGNAT
netip.MustParsePrefix("fd00::/8"), // IPv6 ULA
}
isInternal := false
for _, r := range internalRanges {
if r.Contains(ip) {
isInternal = true
break
}
}
if !isInternal {
c.AbortWithStatusJSON(403, gin.H{"error": "access denied: not from internal network"})
return
}
c.Next()
}
}
使用时挂到特定路由组:
admin := r.Group("/api/admin")
admin.Use(InternalOnly())
admin.GET("/metrics", MetricsHandler)
admin.POST("/debug/clear-cache", ClearCacheHandler)
Nginx 层提前拦截更高效,但要注意头传递
把过滤逻辑下沉到 Nginx 是更优解:省去 Go 层解析、避免无效请求进应用。但必须确保 Nginx 正确透传真实 IP,并禁用伪造可能。
关键配置项:
- 在
http块中启用真实 IP 模块:set_real_ip_from 10.0.0.0/8;等(与 Gin 的SetTrustedProxies一致) - 设置
real_ip_header X-Real-IP;或X-Forwarded-For,并配real_ip_recursive on; - 在 server 或 location 块中加访问控制:
allow 192.168.0.0/16;+deny all;
⚠️ 遗漏点:如果 Nginx 没配 proxy_set_header X-Real-IP $remote_addr; 到后端,Gin 就收不到真实 IP —— 此时中间件和 Nginx 层判断都会失效。
灰度或混合场景下怎么灵活放行
纯 deny all 不现实。常见需求是:内网全放行 + 白名单公网 IP + JWT 认证兜底。这时别堆 if-else,用分层策略更清晰:
- 第一层:Nginx 拦掉明显非法流量(如已知黑产 ASN)
- 第二层:Gin 中间件做 IP 判断(内网 + 白名单 CIDR)
- 第三层:对非内网请求,强制校验
Authorization头中的 JWT,且要求scope包含internal:access
白名单建议用 netip.PrefixSet 存储,配合 atomic.Value 热更新,避免 reload 服务。硬编码在代码里或从文件加载都行,但别用普通 map[string]bool —— 不支持 CIDR,且并发读写不安全。
真正容易被忽略的是:当同时启用 IP 校验和 JWT 校验时,c.ClientIP() 的值仍可能被伪造(若未设可信代理),所以 JWT 校验环节也应基于从 X-Real-IP 解析出的 IP 做日志和风控关联,而不是依赖中间件里已经 abort 的上下文。


















