凌晨灰度发布之后,管理后台列表接口全部超时,浏览器控制台出现了一串 blocked by CORS policy。用 curl 直接打后端域名,返回完全正常;切到页面里就失败。这种情况十之八九不是接口挂了,而是跨域响应头在链路中被改坏了。

顺着请求链路往下拆解:浏览器会先发出 OPTIONS 预检请求,状态码为 200,但是响应头里出现了两条 Access-Control-Allow-Origin,一条来自网关 Nginx,另一条来自后端框架的全局配置。浏览器遇到重复的跨域头就会直接认为是非法的,真正的业务请求根本就不会发送出去。
另外还有一个隐患就是前端开启了凭据携带,但是后端使用了通配符 *。规范中这两者是互斥的,即使去掉重复的头,带有 Cookie 的请求也会被拦截。
修复动作分为三个步骤。第一步是跨域头只保留一层,网关层统一下发,后端去掉 @CrossOrigin 和全局 CorsRegistry 配置,防止叠加。第二步是把 * 替换成前端真实的域名,按照协议、域名、端口进行精确匹配;多域名场景下用白名单动态回写 Origin,并补上 Vary: Origin 来防止缓存串源。第三步是为 OPTIONS 方法单独返回 204,预检缓存设置为 1 小时,减少重复探路。Nginx 中所有的 add_header 指令都加上 always 参数,这样即使出现 401、403 等异常响应也不会丢失跨域头。
重启之后,预检和业务请求都通过了,页面数据也恢复了。之后把“跨域配置单一出口、生产禁通配符、预检必须显式放行”这三条写进了上线检查单里,同样的问题没有再出现。