access-control-allow-origin: "*" 与 allowcredentials: true 共存不安全,因浏览器会直接拒绝响应;正确做法是白名单精确匹配可信域名(如 "https://app.example.com"),且协议、域名、端口须完全一致。

为什么 c.Header("Access-Control-Allow-Origin", "*") 在生产环境不安全
因为 Allow-Credentials: true 和 Access-Control-Allow-Origin: "*" 不能共存。浏览器会直接拒绝这种组合的响应,导致带 Cookie 或 Authorization 的请求失败。错误现象通常是:控制台报错 Response to preflight request doesn't pass access control check: 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'。
真正安全的做法是:只对可信域名做精确回写。比如前端部署在 https://app.example.com,后端就只允许这个 Origin,且必须完全匹配(协议、域名、端口缺一不可)。
- 用
c.Request.Header.Get("Origin")获取真实请求源,再白名单校验 - 白名单必须是完整 URL 字符串,如
"https://app.example.com",不能只写"app.example.com" - 开发环境可临时用
"http://localhost:8080",但上线前必须删掉或替换 - 如果前端有多个合法域名(如 www 和 app),白名单数组要全部列全,不能靠通配符“模糊匹配”
如何用 gin-contrib/cors 做精确域名过滤
官方中间件支持函数式配置,比手动写 Header 更可靠,尤其能自动处理 OPTIONS 预检和缓存逻辑。关键点在于 AllowOrigins 必须传具体字符串切片,而不是 "*"。
示例配置:
router.Use(cors.New(cors.Config{
AllowOrigins: []string{
"https://app.example.com",
"https://www.example.com",
"https://staging.example.com",
},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Content-Type", "Authorization", "X-Requested-With"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
-
AllowCredentials: true必须配合非通配符的AllowOrigins,否则中间件内部会 panic 或静默降级 - 如果前端端口不固定(如本地开发用 3000/8080/5173),建议在配置里显式列出,或通过环境变量注入
-
AllowOriginsFunc可用于动态判断,但要注意性能——不能每次请求都查 DB 或远程服务
手写中间件时最容易漏掉的三个检查点
自己写 CorsMiddleware() 看似简单,但实际线上踩坑最多。核心问题不是 Header 写不写,而是边界逻辑没覆盖。
- 没校验
Origin是否为空:当请求没带Origin头(比如 curl 直接调用),c.Request.Header.Get("Origin")返回空字符串,直接赋值给Access-Control-Allow-Origin会导致浏览器拒绝响应 - 没处理
OPTIONS后提前 abort:必须在c.AbortWithStatus(200)或c.AbortWithStatus(http.StatusNoContent)后立刻 return,否则后续中间件和 handler 还会执行 - 没区分开发/生产环境:硬编码
"*"很容易随代码合并进 master,CI/CD 不拦截就直接上生产
推荐写法片段:
origin := c.Request.Header.Get("Origin")
if origin == "" {
c.Next()
return
}
// 白名单校验
allowed := false
for _, o := range allowedOrigins {
if o == origin {
allowed = true
break
}
}
if !allowed {
c.AbortWithStatus(http.StatusForbidden)
return
}
c.Header("Access-Control-Allow-Origin", origin)
前端 withCredentials 和后端 AllowCredentials 必须严格一致
这是最常被忽略的协同点。只要前端 axios/fetch 设置了 credentials: "include"(或 withCredentials: true),后端就必须同时满足两个条件:明确设置 AllowCredentials: true,且 Access-Control-Allow-Origin 不能是 "*"。
否则现象是:GET/POST 请求能发出去,也能收到 200 响应,但响应体始终为空,浏览器控制台提示 Failed to load response data。
- Vue 项目中,
axios.defaults.withCredentials = true要全局设,但仅限于已知需要鉴权的环境 - fetch 调用必须显式写
{ credentials: "include" },不能依赖默认值 - Gin 中间件一旦启用
AllowCredentials,所有跨域响应都会附带Access-Control-Allow-Credentials: true,哪怕当前请求根本没带 Cookie
精确跨域的本质不是“放开多少”,而是“只放行谁”。白名单域名、凭证开关、预检响应这三者必须咬合,少一个环节,前端就收不到数据。











