带凭证请求失败是因为浏览器强制要求access-control-allow-origin不能为"",必须精确匹配白名单中的origin;正确做法是动态校验origin并仅对允许域名设置响应头,且allowcredentials为true时alloworigins列表中严禁包含""。

为什么直接写 ["*"] 会导致带 cookie 的请求失败
当你的前端设置了 withCredentials: true(比如要传 Cookie 或 Authorization header),浏览器会强制要求后端响应头中 Access-Control-Allow-Origin 不能是 "*",否则直接拦截响应。此时若中间件里还硬写 c.Header("Access-Control-Allow-Origin", "*"),控制台就会报错:Response to preflight request doesn't pass access control check。
根本原因是:带凭证的跨域请求,Origin 必须精确匹配,不能通配。所以必须根据请求头里的 Origin 字段做白名单比对,再动态回写。
- 不要在中间件里无条件设
"*" - 不要用
gin-contrib/cors的AllowOrigins: []string{"*"}配置(哪怕只配一个域名也别混进去) - 如果用了
AllowCredentials: true,AllowOrigins列表里任何一项都不能是"*"
怎么手动写一个只允许多个指定域名的中间件
自己写中间件比依赖第三方库更可控,尤其在需要校验 Origin 是否在白名单内时。核心逻辑是:取 c.Request.Header.Get("Origin"),查是否在预设列表中,命中才写头;否则不写或返回 403。
示例片段:
func Cors() gin.HandlerFunc {
allowedOrigins := []string{
"https://admin.example.com",
"http://localhost:3000",
"https://app.prod.net",
}
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
isAllowed := false
for _, o := range allowedOrigins {
if o == origin {
isAllowed = true
break
}
}
if isAllowed {
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
c.Header("Access-Control-Expose-Headers", "Content-Length")
}
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
- 必须放在
r.Use(...)调用处,且要在所有r.GET/r.POST之前 -
c.AbortWithStatus(http.StatusNoContent)是处理预检请求的标准做法,不能用c.JSON或c.String - 如果 Origin 不在白名单里,中间件什么头都不设,让后续逻辑自然处理(比如返回 403)
用 gin-contrib/cors 时如何安全配置多个域名
如果你倾向用现成库,gin-contrib/cors 的 New 函数支持细粒度控制,但要注意几个关键点:
错误写法:AllowOrigins: []string{"https://*.example.com"} —— 这个库不支持通配符匹配,"https://*.example.com" 会被当成字面量,完全不生效。
正确写法是显式列出完整域名:
router.Use(cors.New(cors.Config{
AllowOrigins: []string{"https://admin.example.com", "http://localhost:3000"},
AllowMethods: []string{"GET", "POST", "OPTIONS"},
AllowHeaders: []string{"Content-Type", "Authorization"},
AllowCredentials: true,
}))
-
AllowOriginFunc比AllowOrigins优先级更高,适合做运行时判断(比如从数据库读白名单) - 一旦启用
AllowCredentials: true,AllowOrigins里绝对不能含"*",否则整个配置会被浏览器忽略 - 这个库默认对 OPTIONS 请求返回 204,不需要额外处理,但前提是中间件注册顺序正确
最容易被忽略的注册顺序问题
中间件必须在路由定义前注册,否则对已注册的路由组无效。常见错误是:
r := gin.Default()
api := r.Group("/api")
api.GET("/user", handler) // ← 这个路由已经注册了
r.Use(Cors()) // ← 中间件加在后面,不生效
正确顺序是:
r := gin.Default()
r.Use(Cors()) // ← 必须放这里
api := r.Group("/api")
api.GET("/user", handler)
- 如果用了
gin.Recovery()或其他中间件,Cors()应该排在最前面(至少在所有业务路由之前) - 中间件内部不能提前调用
c.Abort()或写响应体(如c.JSON),否则 OPTIONS 预检会失败 - 调试时可加日志打印
origin和匹配结果,确认是否真的进了白名单分支
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











