Gin项目应使用gin-contrib/cors而非手写Header,因其需严格注册顺序(r.Use()在路由前)且AllowCredentials为true时AllowOrigins不可含*;net/http则须手动处理OPTIONS并返回204。

直接上结论:Gin 项目里别手写 c.Header().Set() 处理跨域,用 gin-contrib/cors 并严格按顺序注册;原生 net/http 服务则必须自己写中间件,但得先处理 OPTIONS、再设头、最后放行,漏一步就静默失败。
为什么手写中间件在 Gin 里大概率失效
Gin 的 c.Writer 是封装过的响应体,c.Header().Set() 调用时机不对会覆盖或丢失。更关键的是:如果你在路由 handler 里写头,OPTIONS 请求根本不会走到那里——它被 Gin 默认路由逻辑跳过,直接 404。现象是前端控制台报 Response to preflight request doesn't pass access control check,但后端日志里压根没打印任何东西。
正确做法只有两个硬性条件:
-
r.Use(corsMiddleware)必须在r.GET()、r.POST()等所有路由注册之前 - 中间件内部不能在
c.Next()前写响应(比如提前c.JSON()或c.Status()),否则OPTIONS预检无法返回 204 - 若用
gin-contrib/cors,AllowCredentials: true时,AllowOrigins列表里绝不能含"*",否则浏览器直接丢弃响应(Network 面板里都看不到 status)
net/http 手写中间件必须处理 OPTIONS 的三种情况
原生 net/http 没有框架层拦截,OPTIONS 请求默认 404。你写的中间件必须显式捕获并终结它,否则预检失败。
立即学习“go语言免费学习笔记(深入)”;
常见错误写法:if r.Method == "OPTIONS" { w.WriteHeader(200); return } —— 这看似没问题,但漏了两件事:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 没设置
Access-Control-Allow-Methods和Access-Control-Allow-Headers,浏览器会因 header 不全而拒收 - 没设
Access-Control-Allow-Origin,即使只是预检,这个头也必须存在 - 返回 200 不如返回 204(
http.StatusNoContent),更符合规范,且避免空 body 引发 Content-Length 争议
正确结构是:
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin != "" {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
}
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
AllowCredentials 和 AllowOrigins 冲突的真实表现
这不是配置“不生效”,而是浏览器强制行为:只要响应头同时出现 Access-Control-Allow-Origin: "*" 和 Access-Control-Allow-Credentials: "true",整个响应会被丢弃,Network 面板里 status、headers、response 全部为空——你甚至看不到 500 或 401,就像请求没发出一样。
所以动态白名单不是可选项,是必须项:
- 前端带
credentials: 'include'→ 后端必须从r.Header.Get("Origin")取值,比对白名单(注意协议、端口、斜杠结尾) - 白名单不能靠字符串前缀匹配(比如
"https://example.com"不匹配"https://admin.example.com"),得完整相等或手动解析 host - 用
gorilla/handlers.CORS可改用handlers.AllowedOriginsFunc实现运行时校验,比硬编码列表更安全
gin-contrib/cors 配置里最容易忽略的三个字段
很多人只设 AllowOrigins 就以为完事,其实这三个字段不配齐,PUT/DELETE、带 token 的请求、自定义 header 全都会卡在预检阶段:
-
AllowMethods必须显式包含"OPTIONS",否则库内部不识别预检方法 -
AllowHeaders要覆盖前端实际发的所有非简单 header,比如X-Auth-Token、X-Request-ID,漏一个就 403 -
ExposeHeaders如果前端 JS 需要读取响应里的X-Total-Count或Link,必须在这里声明,否则 JS 拿不到
真正麻烦的从来不是“怎么加头”,而是“哪个头漏了导致预检失败”——浏览器报错信息里会明确写出缺失的 header 名,盯着那行字去补就行。

















