因为gin-contrib/cors按Origin头匹配域名而非IP地址,且CORS规范不支持IP控制,强行混用会导致预检失败或响应头冲突;真正实现需自定义中间件,在CORS前校验真实IP(优先X-Real-IP,fallback X-Forwarded-For),用net.IPNet.Contains精确匹配CIDR白名单,未命中则AbortWithStatus(403)。

为什么 gin-contrib/cors 不适合做 IP 黑白名单
因为 gin-contrib/cors 的 AllowOrigins 是按 Origin 请求头匹配域名(如 https://admin.example.com),不是按客户端 IP 地址过滤。Origin 头由浏览器自动携带,服务端无法伪造,但 IP 地址在反向代理后可能被覆盖(比如 Nginx 未设 proxy_set_header X-Real-IP $remote_addr),直接读 c.ClientIP() 可能拿到的是代理内网地址。更关键的是:CORS 规范本身不定义 IP 控制逻辑,强行混用会导致预检失败或响应头冲突。
如何在 Gin 中真正实现 IP 白名单拦截
必须写自定义中间件,在路由处理前校验 IP,且要早于 CORS 中间件执行(否则 OPTIONS 预检请求可能被跳过)。重点是获取真实客户端 IP,并与白名单比对:
- 优先从
X-Real-IP头取值(要求前端或网关透传); fallback 到X-Forwarded-For的第一个非私有地址;最后才用c.ClientIP() - 白名单建议用 CIDR 格式(如
"192.168.1.0/24"),避免硬编码单个 IP 字符串比对 - 若 IP 不在白名单,直接
c.AbortWithStatus(403),不要调用c.Next() - 务必放在所有
r.Use(...)的最前面,例如:r.Use(ipWhitelistMiddleware)紧跟在r := gin.Default()后
示例片段:
func ipWhitelistMiddleware() gin.HandlerFunc {
whitelist := []string{"127.0.0.1/32", "192.168.0.0/16"}
return func(c *gin.Context) {
ip := realIP(c)
for _, cidr := range whitelist {
_, net, _ := net.ParseCIDR(cidr)
if net.Contains(net.ParseIP(ip)) {
c.Next()
return
}
}
c.AbortWithStatus(403)
}
}
func realIP(c *gin.Context) string {
if ip := c.Request.Header.Get("X-Real-IP"); ip != "" {
return ip
}
if ip := c.Request.Header.Get("X-Forwarded-For"); ip != "" {
parts := strings.Split(ip, ",")
for _, p := range parts {
p = strings.TrimSpace(p)
if !strings.HasPrefix(p, "10.") && !strings.HasPrefix(p, "192.168.") && !strings.HasPrefix(p, "172.16.") {
return p
}
}
}
return c.ClientIP()
}
IP 黑名单和 CORS 共存时的顺序陷阱
常见错误是把 IP 拦截中间件放在 cors.New(...) 之后,导致 OPTIONS 预检请求绕过 IP 检查——浏览器发 OPTIONS 时没带 Origin 头(或带了但不关键),中间件误判为“合法”,放行后 CORS 中间件再加头,结果黑 IP 用户也能完成预检,后续实际请求照样通过。正确顺序必须是:
-
r.Use(ipBlacklistMiddleware)→ 最先执行,拦住黑名单 IP 的所有请求(含 OPTIONS) -
r.Use(cors.New(...))→ 仅对放行的请求添加 CORS 头 -
r.GET("/api/data", handler)→ 最后注册业务路由
注意:ipBlacklistMiddleware 内不能调用 c.Writer.WriteHeader() 或写响应体,否则会破坏 CORS 中间件对 OPTIONS 的 204 响应逻辑。
生产环境必须检查的三个隐藏点
IP 白名单看似简单,但线上常因以下原因失效:
- Nginx 或 Cloudflare 没配
proxy_set_header X-Real-IP $remote_addr,X-Forwarded-For被污染成多个 IP 串(如"203.0.113.1, 198.51.100.1"),代码只取第一个却忽略真实源 - 白名单用了
"0.0.0.0"或空字符串做兜底,导致任意 IP 都匹配成功 - 启用了
AllowCredentials: true的 CORS 配置,但 IP 拦截中间件返回 403 时没清除已写的Access-Control-Allow-Origin头,造成响应头冲突被浏览器拒绝
最稳妥的做法是:IP 拦截只做 AbortWithStatus(403),不碰任何响应头;CORS 头全部交给 gin-contrib/cors 统一管理,两者职责彻底分离。


















