应使用 gin-contrib/cors 中间件而非手写 header,因其完整处理预检(options)、凭据兼容及缓存头;手写易漏预检响应、未 abort 导致 404 或 panic,且必须在路由注册前调用。

直接用 github.com/gin-contrib/cors 中间件,别手写 Header —— 手写容易漏预检(OPTIONS)处理、凭据兼容或缓存头,90% 的跨域失败都源于此。
为什么不能只写几个 c.Header() 就完事?
浏览器对非简单请求(如带 Authorization 头、Content-Type: application/json、或用 PUT/DELETE)会先发一次 OPTIONS 预检。手写中间件若没显式处理 Method == "OPTIONS" 并提前终止链路,就会让请求继续往下走,导致路由未匹配、404 或 panic。
常见错误现象:
- 控制台报错
Response to preflight request doesn't pass access control check - 前端发了请求,但 Network 面板里看不到实际接口调用,只有 OPTIONS 返回 200 或 404
- 带 cookie 的请求始终被拒绝,即使写了
Access-Control-Allow-Credentials: true
根本原因:手写逻辑没覆盖预检响应的完整头集合,或没调用 c.AbortWithStatus(http.StatusNoContent) 阻断后续执行。
用 gin-contrib/cors 的三种典型配置场景
先安装:go get github.com/gin-contrib/cors
推荐按需选择,而非无脑 cors.Default():
-
开发环境快速验证:用
cors.Default(),它等价于允许所有源 + 所有方法 + 所有头,但不支持凭据(credentials),所以登录态无法透传 -
需要携带 cookie / token:必须设
AllowCredentials: true,且AllowOrigins不能为"*",得写死前端地址,例如[]string{"http://localhost:3000", "https://prod.example.com"} -
动态校验来源(如多租户 SaaS):用
AllowOriginFunc,比如只放行子域名匹配的请求:AllowOriginFunc: func(origin string) bool { return strings.HasSuffix(origin, ".myapp.com") }
示例(生产可用):
config := cors.Config{
AllowOrigins: []string{"https://admin.myapp.com", "http://localhost:3000"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Content-Type", "Authorization", "X-Requested-With"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}
r.Use(cors.New(config))
最容易被忽略的两个硬性条件
即使配置全对,中间件也**必须在注册任何路由之前调用**,否则对已定义的路由不生效:
- ❌ 错误顺序:
r.GET("/api/user", handler); r.Use(cors.New(...))→ 跨域头不会加到/api/user - ✅ 正确顺序:
r.Use(cors.New(...)); r.GET("/api/user", handler)
另一个隐形坑:AllowCredentials: true 时,前端 fetch 或 axios 必须显式开启凭据:
fetch("/api/user", { credentials: "include" })axios.get("/api/user", { withCredentials: true })
缺这一步,浏览器压根不会发送 cookie,后端也收不到,跟 CORS 配置无关。
真正卡住人的从来不是“怎么配”,而是预检是否被正确响应、凭据开关是否前后端一致、中间件加载时机是否正确——这三处一错,就只能在 Network 面板里反复看 OPTIONS 响应头有没有 Access-Control-Allow-Origin 和 Access-Control-Allow-Credentials。











