网关必须拦截OPTIONS预检请求并正确配置CORS,包括禁用OptionsPassthrough、设置AllowCredentials时禁止使用通配符Origin、动态白名单校验、统一在网关层处理以避免策略冲突和缓存污染。

网关必须拦截 OPTIONS 请求,不能转发
浏览器发起带凭证或自定义 Header 的跨域请求前,会先发一个 OPTIONS 预检请求。如果网关不拦截、而是把 OPTIONS 转发给下游服务,大概率返回 404 或 405 ——因为后端服务未必注册了 OPTIONS 路由。此时浏览器收不到 Access-Control-Allow-Methods 等头,直接报错:Response to preflight request doesn't pass access control check。
实操建议:
-
rs/cors默认不拦截OPTIONS,必须显式设OptionsPassthrough: false(Kratos Gateway 已默认开启该行为) -
gorilla/handlers.CORS()默认拦截并响应OPTIONS,更省心,但需确认你没在下游又配了一套 CORS 中间件,否则可能重复写头 -
OPTIONS响应状态码应为http.StatusNoContent(204)或http.StatusOK(200),且响应体必须为空
AllowCredentials 为 true 时,AllowedOrigins 不能用 "*"
这是浏览器硬性限制:当后端设了 Access-Control-Allow-Credentials: "true",同时 Access-Control-Allow-Origin 是 "*",浏览器会静默拒绝整个响应——连错误都不会报在控制台,只显示“CORS error”。前端 fetch 设置了 { credentials: 'include' },后端就必须同步满足三项:
-
Access-Control-Allow-Credentials: "true"(字符串,不是布尔值) -
Access-Control-Allow-Origin必须是单个明确域名,如"https://app.example.com" - 响应中必须含
Vary: Origin头(rs/cors和gorilla/handlers会自动加,手写中间件要自己补)
开发环境可写死:[]string{"http://localhost:3000", "http://127.0.0.1:3000"};生产环境建议从 Origin 头提取域名,白名单校验后再回写,避免反射型漏洞。
立即学习“go语言免费学习笔记(深入)”;
动态 Origin 白名单比静态列表更安全也更灵活
硬编码 AllowedOrigins: []string{"https://admin.example.com", "https://web.example.com"} 在多租户、灰度发布或前端频繁换域名的场景下难维护。更合理的方式是用回调函数做运行时校验:
-
rs/cors支持AllowedOriginsFunc,可从配置中心或数据库查白名单 -
gorilla/handlers不支持回调,只能手动写中间件解析r.Header.Get("Origin"),注意校验协议、端口、末尾斜杠(如https://example.com≠https://example.com/) - 子域名通配(如
"*.example.com")不被标准支持,需自己做strings.HasSuffix(origin, ".example.com")类似判断,并排除evil.example.com这类恶意构造
网关层统一配 CORS 比每个服务单独配更可靠
API 网关是所有流量入口,统一处理 CORS 能避免下游服务漏配、误配或策略冲突。尤其当微服务技术栈不一时(比如有的用 Gin、有的用 Kratos、有的用原生 net/http),各自实现 CORS 很容易出现:
- 响应头重复(两个
Access-Control-Allow-Origin),触发浏览器拒绝 - 某些服务忘了处理
OPTIONS,导致预检失败 - 同一路径在不同服务上允许的
Origin不一致,策略混乱
真正容易被忽略的是缓存污染:如果没加 Vary: Origin,CDN 或反向代理可能把针对 https://a.com 的 CORS 响应缓存下来,错误地返回给 https://b.com 的请求——这个 bug 往往在线上压测或灰度阶段才暴露,排查成本高。


















