彻底搞懂Access-Control-Allow-Headers跨域请求头设置

IT人卫利 安全 阅读 885
赞 16 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

先说结论: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,可能就过不去。建议统一按规范来,用常见的驼峰或短横线命名,比如 AuthorizationX-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 缓存那些事儿。如果你也在这些问题上翻过车,欢迎评论区交流,咱们互相避雷。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论