必须返回204 no content,因rfc 7231规定其为预检成功语义,200会导致chrome/safari静默失败;需搭配abort()终止中间件链,且access-control-allow-origin不可为*(启用credentials时须白名单匹配origin)。

OPTIONS预检请求返回204而非200
浏览器对跨域预检请求(OPTIONS)的响应状态码非常敏感。用 c.AbortWithStatus(200) 看似能“快速放行”,但实际会触发部分浏览器(尤其是 Chrome 和 Safari)静默失败——后续 GET/POST 请求根本发不出去。
必须返回 204 No Content,且不带响应体。这是 RFC 7231 明确规定的预检成功语义。
- ✅ 正确写法:
c.AbortWithStatus(204)或c.Status(204)后紧跟c.Abort() - ❌ 错误写法:
c.AbortWithStatus(200)、c.JSON(200, "")、c.String(200, "") - 注意:如果用了
c.Status(204)却没调c.Abort(),后续中间件仍可能执行,导致 Header 冲突或重复写入
Access-Control-Allow-Credentials 与 Origin 冲突
当需要前端携带 Cookie 或 Authorization header(即设置了 Access-Control-Allow-Credentials: "true"),Access-Control-Allow-Origin 就绝不能填 "*" —— 浏览器会直接拒绝整个响应。
生产环境必须动态匹配或白名单校验 Origin 头。
- 开发阶段可硬编码:
c.Header("Access-Control-Allow-Origin", "http://localhost:3000") - 生产建议用白名单数组逐个比对:
allowedOrigins := []string{"https://app.example.com", "https://admin.example.com"} - 不要用字符串包含判断(如
strings.Contains(origin, "example.com")),易被绕过 - 若需支持多个子域,可解析 Host 后做前缀匹配,但务必校验协议+端口一致性
gin-contrib/cors 中间件的配置陷阱
gin-contrib/cors 封装了标准逻辑,但默认行为容易踩坑。比如 cors.Default() 在生产中等同于开放全部来源,且未设 AllowCredentials,和前端 withCredentials: true 不匹配。
直接使用时,必须显式传入完整 Config。
- ✅ 推荐初始化方式:
router.Use(cors.New(cors.Config{ AllowOrigins: []string{"https://your-domain.com"}, AllowCredentials: true, MaxAge: 12 * time.Hour })) - ❌ 避免:
router.Use(cors.Default())(仅限本地调试) - 注意:
AllowHeaders必须包含前端实际发送的自定义头,比如"Authorization"、"X-Trace-ID",漏掉一个就会导致预检失败 -
ExposeHeaders是给前端 JS 读取的响应头白名单,如需读X-Total-Count,必须显式声明
中间件注册顺序错误导致失效
Gin 中间件生效依赖注册时机。如果把 r.Use(CorsMiddleware()) 放在路由注册之后,它对已定义的路由组完全不起作用。
常见反模式是先挂路由,再挂中间件,结果只有后续新增路由才受控。
- ✅ 正确顺序:创建 router →
r.Use(...)→ 定义所有路由(包括r.Group) - ❌ 错误顺序:
r.GET(...)→r.Use(...)→ 新增路由无效 - 特别注意:Gin 的
Group是独立的路由树节点,其内部中间件需单独Use,全局中间件只影响注册时已存在的路由节点 - 验证是否生效:curl -I -X OPTIONS http://localhost:8080/any-path,检查响应头是否含
Access-Control-Allow-Origin
真正难的不是写几行 Header,而是让每个请求都稳定通过预检、凭据正确透传、且不暴露攻击面。Origin 动态校验、204 状态码、中间件加载顺序——这三处任何一个出错,前端就只会看到“跨域被阻止”,连错误细节都不报。











