彻底搞懂Access-Control-Allow-Headers跨域请求头设置
我的写法,亲测靠谱
先说结论:Access-Control-Allow-Headers 这玩意儿我一开始真没当回事,觉得不就是配个响应头嘛,直到线上接口突然挂了一片,用户登不上、数据拉不了,我才意识到——这东西不是“能用就行”,是得写对。
现在我的标准做法是在后端(比如 Node.js 的 Express)里这样写:
app.use((req, res, next) => {
const origin = req.headers.origin;
const allowedOrigins = [
'https://jztheme.com',
'https://staging.jztheme.com'
];
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
res.header('Access-Control-Allow-Credentials', 'true');
}
// 关键来了:只允许必要的 headers
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
// 如果是预检请求,直接返回 204
if (req.method === 'OPTIONS') {
res.status(204).end();
return;
}
next();
});
你可能会问,为啥要手动指定这些 header?不能用 * 吗?听我说完你就懂了。
这个配置的核心点在于:只放行真正需要的 headers。我之前图省事写成 *,结果某些浏览器下带 Authorization 的请求直接失败,折腾了半天才发现是规范限制——当请求包含凭据(如 cookies 或 Authorization 头)时,Access-Control-Allow-Origin 和 Access-Control-Allow-Headers 都不能是 *。
所以你看,看似偷懒的写法反而把自己坑了。
这几种错误写法,别再踩坑了
下面这几个我都见过,甚至自己也写过……血泪教训。
- 滥用通配符 *:
像这样:res.header('Access-Control-Allow-Headers', '*');表面看万能,实则埋雷。一旦前端发了个带
Authorization的请求,Chrome 直接报错:Request header field authorization is not allowed by Access-Control-Allow-Headers。因为 * 和凭据模式互斥,别不信,官方 spec 就这么写的。 - 漏掉关键 header:
比如只写了 Content-Type,忘了 Authorization:res.header('Access-Control-Allow-Headers', 'Content-Type');结果前端用 JWT 发请求时加了 Authorization 头,预检就挂了。这种问题在开发环境可能还发现不了(因为同源),一上生产就炸。
- 动态拼接但没过滤:
有人为了“灵活”搞成这样:const requestHeaders = req.headers['access-control-request-headers']; res.header('Access-Control-Allow-Headers', requestHeaders);看着很聪明是吧?但这是安全漏洞。攻击者可以伪造任意 header 名让服务器回显,虽然实际不会执行,但属于信息泄露风险,而且容易被扫描工具抓出来当高危项打脸。
- 大小写敏感处理不当:
HTTP Header 本身不区分大小写,但有些中间件或代理会原样反射。如果你服务端严格匹配小写,而前端传了authorization,可能就过不去。建议统一按规范来,用常见的驼峰或短横线命名,比如Authorization、X-Api-Key这种。
实际项目中的坑
去年我们有个 H5 页面嵌在 App 里,App 通过 WebView 注入了一个叫 X-Device-Token 的头,用来标识设备。一开始后端没把这个加进 Allow-Headers,前端死活发不出去,控制台只显示“Network Error”,连具体原因都不给。
我折腾了快两小时,最后抓包才发现是预检请求被拒。加上之后就好了:
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-Device-Token');
这里注意:自定义 header 一定要明确列出,不然浏览器根本不让它过预检关。
还有一次更离谱,测试同事用 Postman 调接口没问题,但页面里就是不行。查到最后发现他 Postman 没走 OPTIONS 预检,直接发了 POST,而浏览器会先发一次 OPTIONS。所以我们必须确保 OPTIONS 路由也能正确返回 CORS 头,否则预检失败,主请求压根不会发出。
我的建议是:所有 API 路由都走统一中间件处理 CORS,不要零散设置,避免遗漏。
关于性能和灵活性的一点取舍
你说能不能动态判断哪些 header 放行?技术上当然可以,但我一般不用。为什么?
第一,增加复杂度;第二,容易出错;第三,基本没收益。
我们又不是通用网关,每个项目用的 headers 基本固定。把常用的列出来,半年都不用动一次。就算新增一个,也是配合业务上线一起改,可控得很。
反倒是那种“全量透传”的做法,看着省事,后期维护起来要命。审计的时候谁都说不清到底开了多少口子。
另外提一嘴,如果用了 Nginx 做反向代理,别忘了它也可能覆盖或拦截你的响应头。我之前就被坑过:后端明明写了 header,Nginx 默认配置却没允许转发,结果前端收不到。解决办法是在 Nginx 加:
add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" always;
记得加 always,不然 4xx 响应会不带这个头,调试起来更麻烦。
总结一下我现在的标准动作
1. 明确列出所需 headers,绝不使用 *(尤其涉及凭据时)
2. 自定义 header 必须显式声明
3. 统一通过中间件处理,避免分散配置
4. 确保 OPTIONS 请求能正常返回 CORS 头
5. 配合 Nginx 等反向代理时检查是否透传成功
6. 定期 review 允许的 headers 列表,及时清理废弃项
以上是我踩坑后的总结,希望对你有帮助。这个机制看似简单,但细节决定成败。一个小疏忽就能让你在联调时对着控制台干瞪眼一整天。
顺便说一句,CORS 的坑远不止这一处,后续我还打算写写 Vary、Credentials、Preflight 缓存那些事儿。如果你也在这些问题上翻过车,欢迎评论区交流,咱们互相避雷。

暂无评论