gin本身不处理跨域,必须手动写中间件或引入gin-contrib/cors;自定义中间件易因注册顺序错、提前写响应头、options未返回204或allowcredentials为true时设origin为*而失败。

直接上结论:Gin 本身不处理跨域,必须手动写中间件或引入 gin-contrib/cors;但自己写的中间件如果顺序错、提前写头、或没正确响应 OPTIONS,前端就会卡在预检失败——典型报错是 Response to preflight request doesn't pass access control check。
为什么自定义中间件比用 gin-contrib/cors 更容易出错
因为跨域涉及浏览器预检(OPTIONS)和实际请求两阶段,而自定义中间件容易忽略以下关键点:
- 没在
c.Next()之后统一写响应头,导致 GET 正常、POST 报错 - 对 OPTIONS 请求直接
c.Abort()或返回 JSON,但浏览器要求它必须返回 204 状态且不能有 body - 允许凭证(
AllowCredentials: true)时,却把Access-Control-Allow-Origin设为"*",浏览器直接拒收整个响应 - 中间件注册位置不对:放在
r.GET()之后,导致部分路由没被拦截
手写 CORS 中间件的最小可行写法
不用第三方库,5 行代码就能跑通多数场景,关键是控制好执行时机和状态码:
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.GetHeader("Origin")
if origin != "" {
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
}
// 预检请求必须立刻返回,且不能调用 c.Next()
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
注意这几点:
-
c.AbortWithStatus(204)是必须的,不能用c.JSON或c.String,否则浏览器判定预检失败 - 所有
c.Header()必须在c.Next()前设置,但仅对非 OPTIONS 请求生效;OPTIONS 走的是提前 return 分支 - 这里动态回传
Origin,比硬编码域名列表更灵活,但需自行校验白名单(见下一条)
如何安全地支持多个指定域名(不依赖 *)
如果后端只允许 https://admin.example.com 和 https://app.example.com,就不能靠 "*",得做白名单匹配:
allowedOrigins := []string{"https://admin.example.com", "https://app.example.com"}
origin := c.GetHeader("Origin")
for _, o := range allowedOrigins {
if o == origin {
c.Header("Access-Control-Allow-Origin", origin)
break
}
}
常见疏漏:
- 没校验
Origin是否为空,导致空字符串也被放行 - 用
strings.Contains(origin, "example.com")这类模糊匹配,可能被https://evil.example.com绕过 - 忘记同时设置
Access-Control-Allow-Credentials: "true",导致带 cookie 的请求仍失败
注册顺序和全局生效范围必须严格按这个顺序
中间件必须在任何路由注册前挂载,否则部分接口不会走 CORS:
r := gin.Default()
r.Use(corsMiddleware()) // ✅ 必须在这一步
// 所有路由定义都在这之后
r.GET("/health", func(c *gin.Context) { c.String(200, "ok") })
api := r.Group("/api")
api.POST("/login", loginHandler)
如果写成这样就失效:
r := gin.Default()
r.GET("/health", ...) // ❌ 先注册路由,再 Use,/health 就不走 corsMiddleware
另外,gin-contrib/cors 的 Config 里 AllowOrigins 是精确匹配,填 ["https://*.example.com"] 不生效——它不支持通配符语法,只认完整字符串。











