options请求必须返回204且立即终止中间件链,不能返回200;allowcredentials为true时access-control-allow-origin不可为"*",须精确匹配白名单中的origin;gin-contrib/cors必须置于鉴权中间件之前,header应通过c.writer.header()设置。

OPTIONS请求必须返回204且立即终止中间件链
浏览器预检请求(Preflight)要求成功响应必须是204 No Content,不是200 OK,也不是200带空JSON。Chrome、Safari对非标准状态码极其敏感,返回200会导致后续真实请求被静默拦截。
关键动作只有两步:设响应头 + c.AbortWithStatus(204),之后必须return,不能调用c.Next()或任何写响应体的操作。
- 错误写法:
c.JSON(200, gin.H{})或c.Status(200) - 正确写法:
c.AbortWithStatus(204)(它会清空响应体、设状态码、并终止链) - 若用
gin-contrib/cors,它已内置该逻辑;手写中间件时务必手动实现
AllowCredentials为true时Access-Control-Allow-Origin不能是"*"
只要前端发请求时带了withCredentials: true(比如传Cookie或Authorization header),服务端就不能返回Access-Control-Allow-Origin: "*"——浏览器会直接拒绝整个响应,控制台只显示“CORS error”,不报具体原因。
必须显式匹配Origin值,或从白名单中精确比对:
- 开发环境可硬编码:
[]string{"http://localhost:3000", "http://127.0.0.1:5173"} - 生产环境建议动态查库或配置中心,避免改代码发版
- 注意协议、端口、斜杠都要完全一致:
"https://example.com"≠"https://example.com/"(后者可能被某些客户端发出)
gin-contrib/cors中间件必须放在鉴权中间件之前
OPTIONS预检请求没有body、没有token、甚至可能没带Authorization header,如果把它丢给JWT中间件处理,大概率会因“token缺失”而提前Abort(),导致预检失败,真实请求永远发不出去。
gin-contrib/cors的职责就是无条件响预检,所以它必须是第一个中间件:
- ✅ 正确顺序:
r.Use(cors.New(config))→r.Use(JWTAuth())→r.GET(...) - ❌ 错误顺序:
r.Use(JWTAuth())→r.Use(cors.New(config))(后者根本不会被执行) - 调试技巧:用
curl -X OPTIONS -H "Origin: https://a.com" -I http://localhost:8080/api/user看响应头是否含Access-Control-Allow-Origin
手写中间件时Header设置必须用Writer.Header()而非c.Header()
c.Header()只是往c.Writer.Header()里写,但Gin中间件执行时机早于路由匹配,若在c.Next()前调用c.Header(),部分header可能被后续中间件或handler覆盖(尤其是Content-Type这类易冲突字段)。
更稳妥的做法是统一在c.Next()之后、或OPTIONS分支内直接操作c.Writer.Header():
- 推荐写法:
c.Writer.Header().Set("Access-Control-Allow-Origin", origin) - 避免写法:
c.Header("Access-Control-Allow-Origin", origin)(尤其在c.Next()前) - 注意:
Access-Control-Expose-Headers必须显式列出前端JS要读的自定义响应头,如"X-Request-ID",否则JS拿不到
AllowCredentials: true)和白名单(AllowOrigins)必须成对出现,且白名单不能为空切片——哪怕只配一个开发地址,也不能留空或填["*"]。**











