不能只靠c.clientip()做区域限制,必须先配置settrustedproxies并正确设置nginx头以获取真实ip,再用geoip2离线解析国家代码,否则地域拦截逻辑必然失效。

直接结论:不能只靠 c.ClientIP() 做区域限制,必须先解决真实 IP 获取问题,再加载本地 GeoIP 数据库解析,否则所有“地域拦截”逻辑都建立在错误的 IP 上。
为什么 c.ClientIP() 拿不到真实用户 IP
你写的中间件里调用 c.ClientIP(),返回的很可能是 10.0.0.1、127.0.0.1 或 Nginx/Cloudflare 的内网地址——因为 Gin 默认不信任任何代理。它只认直连请求,而生产环境几乎都有反向代理。
- 必须在
r := gin.Default()之后立即调用r.SetTrustedProxies([]string{"10.0.0.0/8", "192.168.0.0/16", "173.245.48.0/20"}),填入你实际的负载均衡或 CDN 网段(Cloudflare 官方 IP 段可查 MaxMind 或官方文档) - Nginx 配置里必须有
proxy_set_header X-Real-IP $remote_addr;和proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - 验证方式:在中间件里打印
c.Request.Header.Get("X-Forwarded-For")和c.ClientIP(),二者应一致且不是私有地址
用 geoip2 库做离线地理解析
别调 ipapi.co 这类在线 API——网络延迟、配额、超时、单点故障全都会让你的限流中间件卡住或误判。GeoLite2 City 是免费的,geoip2 库(官方推荐)支持纯内存加载、无 goroutine 阻塞、并发安全。
- 下载
GeoLite2-City.mmdb(需注册 MaxMind 账号,免费 License Key 可用) - 初始化一次,全局复用:
db, _ := geoip2.Open("GeoLite2-City.mmdb");不要每次请求都Open - 解析示例:
record, err := db.City(net.ParseIP(ip)),然后取record.Country.IsoCode或record.City.Names["zh-CN"] - 注意:
record.Country.IsoCode是大写字符串(如"CN"),匹配时别忽略大小写
按国家/地区做访问控制的中间件写法
核心是把 IP 解析和策略判断收进一个中间件函数,且必须放在鉴权中间件之后(避免未登录用户绕过地理检查),但要在业务 handler 之前。
- 策略配置建议用 map[string]bool 存白名单或黑名单:
allowedCountries := map[string]bool{"CN": true, "SG": true} - 解析失败(如 IP 不在数据库中)应默认放行或拒绝?推荐默认放行,避免因数据缺失导致大面积 403
- 务必加
defer record.Close()(如果用了db.City()返回的*geoip2.City),否则文件句柄泄漏 - 示例关键片段:
func GeoBlockMiddleware(db *geoip2.Reader, blockedCountries map[string]bool) gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
record, err := db.City(net.ParseIP(ip))
if err != nil {
// 解析失败,不阻断
c.Next()
return
}
defer record.Close()
countryCode := record.Country.IsoCode
if countryCode == "" || blockedCountries[countryCode] {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "region not allowed"})
return
}
c.Next()
}
}
容易被忽略的边界情况
真正上线后出问题的,往往不是主流程,而是这些点:
- IPv6 地址解析:GeoLite2 支持 IPv6,但
net.ParseIP()返回的net.IP对 IPv6 是 16 字节,确保你的db.City()调用能处理(geoip2默认支持) - 代理链过长:若经过多层代理(比如 Nginx → Traefik → Gin),
X-Forwarded-For可能含多个 IP,c.ClientIP()默认取最左,但你要确认哪一层才是真实出口(通常是最右非内网 IP) - 数据库更新:GeoLite2 每月更新,但
.mmdb文件不会自动热加载;建议启动时校验文件修改时间,或用fsnotify监听变更后 reload*geoip2.Reader - 性能开销:单次
db.City()在 SSD 上约 0.1–0.3ms,若 QPS 过万,考虑加内存缓存(如freecache),key 为 IP,value 为 country code,TTL 设 24h 即可











