结论:禁用“”通配符,必须动态校验Origin头,否则凭据泄露、CSRF风险极高;因浏览器硬性禁止Access-Control-Allow-Origin: “”与credentials共存,会直接拒绝预检,导致Cookie/JWT无法传递。

直接说结论:用 gin-contrib/cors 的白名单配置,禁用 * 通配符,且必须校验 Origin 头——否则凭据泄露、CSRF 风险极高。
为什么不能用 Access-Control-Allow-Origin: "*" + Access-Control-Allow-Credentials: "true"
浏览器会直接拒绝这种组合,控制台报错:The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'。这是硬性安全限制,不是 Gin 配置问题,改代码也绕不过去。
常见错误现象:
- 前端带
withCredentials: true,后端却设了"*",请求卡在预检(OPTIONS)后不发实际请求 - 开发时用
"*"跑通了,上线后登录态(Cookie/JWT)传不过去,Set-Cookie被浏览器丢弃
正确做法是动态比对 c.Request.Header.Get("Origin"),只放行可信域名:
func CorsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
// 白名单严格匹配,含协议+端口
if origin == "https://admin.example.com" || origin == "https://app.example.com:3000" {
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,PATCH,OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type,Authorization,X-Requested-With")
c.Header("Access-Control-Expose-Headers", "X-Total-Count")
}
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
gin-contrib/cors 的 AllowOriginFunc 怎么用才安全
它比手动写 if 判断更灵活,但容易忽略两个关键点:空 Origin 和协议降级。
使用场景:
- 需要支持多个子域(如
user.example.com、api.example.com) - 测试环境需临时放开 localhost 不同端口
参数差异与坑:
-
AllowOriginFunc返回true时,Access-Control-Allow-Origin值会自动设为请求头里的Origin字符串——所以你仍要自己做校验,不能无脑返回true - 若请求没带
Origin(比如 curl 直接调),函数入参是空字符串,必须显式处理,否则可能误放行 - 不要用
strings.Contains(origin, "example.com"),攻击者可注册evil-example.com绕过
推荐写法:
config := cors.Config{
AllowOriginFunc: func(origin string) bool {
if origin == "" {
return false // 拒绝无 Origin 的请求
}
allowed := []string{
"https://admin.example.com",
"https://app.example.com:3000",
"http://localhost:5173", // 仅限开发
}
for _, a := range allowed {
if origin == a {
return true
}
}
return false
},
AllowCredentials: true,
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"},
AllowHeaders: []string{"Content-Type", "Authorization", "X-Requested-With"},
}微服务间调用要不要开 CORS
完全不需要。CORS 是浏览器施加的限制,纯 Go 服务之间 HTTP 调用(如 http.Client)不走 CORS 流程。
容易踩的坑:
- 把网关层(如 Kong、Traefik)和业务微服务都配了 CORS,重复设置导致响应头混乱
- 前端直连某个微服务(绕过网关),此时才需要该服务单独配 CORS;但架构上应禁止这种直连,统一走 API 网关
- 健康检查接口(如
/healthz)被前端调用,误配 CORS 后暴露内部拓扑——这类接口建议默认禁用 CORS,或仅允许网关 IP 访问
性能影响:CORS 中间件本身开销极小,但 AllowOriginFunc 里做正则或网络请求(如查 Redis 白名单)会拖慢预检响应,务必本地缓存白名单。
OPTIONS 预检请求被反复触发怎么办
根本原因是 MaxAge 设得太小或没设,浏览器无法缓存预检结果。
默认行为:
- 不设
MaxAge时,浏览器每次非简单请求前都发一次 OPTIONS - 设了
MaxAge: 86400(24 小时),同一 Origin+Method+Headers 组合下,24 小时内只预检一次
注意点:
-
MaxAge单位是秒,不是毫秒——写成86400000就失效 - 如果前端动态改
Authorization头值(如 JWT 过期重刷),浏览器视为不同请求,仍会重预检 - 调试时用
curl -I -X OPTIONS http://...看响应头是否含Access-Control-Max-Age
最简修复:
config := cors.DefaultConfig() config.MaxAge = 24 * time.Hour // 显式设为 24 小时 router.Use(cors.New(config))
真正难的是白名单维护——线上环境域名变更频繁,硬编码在代码里会导致每次发布都要改,应该把白名单抽到配置中心或数据库,中间件启动时加载并定期刷新。这点多数人一开始都忽略,直到某天运营同事上线新活动页,跨域突然挂了才反应过来。


















