彻底搞懂 Access-Control-Allow-Headers 的跨域配置陷阱与实战技巧

小晴文 安全 阅读 1,208
赞 24 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

在前后端分离的项目里,跨域请求太常见了。我早期总以为只要后端加个 Access-Control-Allow-Origin: * 就万事大吉,结果一加上自定义 header,比如 X-Auth-Token 或者 Content-Type: application/json,浏览器直接报错:「Request header field X-Auth-Token is not allowed by Access-Control-Allow-Headers」。

彻底搞懂 Access-Control-Allow-Headers 的跨域配置陷阱与实战技巧

折腾几次后才明白,Access-Control-Allow-Headers 这个响应头不是可有可无的装饰品,而是必须显式声明你前端要发哪些自定义 header。我现在的做法是:后端明确列出所有允许的 header,绝不偷懒用通配符(后面会讲为什么)。

比如我用 Node.js + Express 写接口,一般这样处理:

app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'https://your-frontend.com');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Auth-Token, X-Custom-Header');
  res.header('Access-Control-Allow-Credentials', 'true');
  
  if (req.method === 'OPTIONS') {
    return res.status(200).end();
  }
  next();
});

这里我把常见的、项目里实际用到的 header 都列出来了。重点是:只写项目真正需要的,不多不少。比如我们用了 Authorization 做 JWT 鉴权,也用了自定义的 X-Auth-Token(历史原因),那就都加上。别一股脑把所有可能的 header 全塞进去,既不安全也不清晰。

这几种错误写法,别再踩坑了

我见过太多人在这上面翻车,自己也踩过,总结几个典型的反面案例:

  • * 通配符:比如 res.header('Access-Control-Allow-Headers', '*')。看起来省事,但 Chrome 和 Firefox 早就禁止了这种用法——当请求携带 credentials(比如 cookies)时,Access-Control-Allow-Headers 不允许用 *。你一上线就发现登录态传不过去,查半天才发现是这个原因。
  • 漏掉 Content-Type:很多人以为只有自定义 header 才需要声明,其实当你发 application/json 的请求时,浏览器会自动带 Content-Type,而它属于“非简单请求头”,必须显式允许。否则 POST 请求直接被拦。
  • 大小写问题:HTTP header 是大小写不敏感的,但有些老框架或中间件会做字符串匹配。我曾经在一个 PHP 项目里,前端发的是 X-Auth-Token,后端配的是 x-auth-token,结果某些代理环境下匹配失败。后来统一改成首字母大写的驼峰格式,避免这类玄学问题。
  • 只在主请求里返回,忘了 OPTIONS 预检:CORS 预检请求(OPTIONS)必须包含完整的 Access-Control-Allow-Headers,否则浏览器根本不会发真正的请求。我见过有人只在业务接口里加了这个头,但没在 OPTIONS 路由里处理,导致跨域请求直接卡在预检阶段。

实际项目中的坑

有一次我们在对接一个第三方 API,对方文档写得很模糊,只说“支持 CORS”,但没提具体允许哪些 header。我们前端发了个带 X-API-Key 的请求,结果被拦。联系对方后,他们临时加了 Access-Control-Allow-Headers: X-API-Key,但又忘了加 Content-Type,导致 JSON 请求还是失败。最后我们不得不改用 query 参数传 key,虽然不优雅,但能跑起来。

还有一次,我们的网关层(Nginx)做了统一的 CORS 配置,但某个微服务自己又加了一层 CORS 头,导致响应头重复。浏览器对重复的 CORS 头容忍度很低,直接报错。排查了半天才发现是 Nginx 和应用层双重配置冲突。后来我们定下规矩:CORS 统一由网关处理,业务服务不再设置相关头。

另外,开发环境和生产环境的 origin 不同,千万别写死。我习惯把允许的 origin 放在配置文件里,根据环境动态设置:

const allowedOrigins = process.env.NODE_ENV === 'production'
  ? ['https://prod.your-frontend.com']
  : ['http://localhost:3000', 'https://dev.your-frontend.com'];

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (allowedOrigins.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin);
  }
  // ... 其他 headers
});

这样既能保证安全,又避免本地调试时跨域问题。

一点不完美的妥协

理想情况下,我们应该严格控制每个接口允许的 header,但现实是,很多项目迭代快,前端经常临时加个 header 调试,后端又不能每次都改代码。所以我在内部项目里,有时会妥协:维护一个“白名单数组”,动态判断是否允许。

const ALLOWED_HEADERS = [
  'Origin',
  'X-Requested-With',
  'Content-Type',
  'Accept',
  'Authorization',
  'X-Auth-Token',
  'X-Debug-Flag' // 临时调试用
];

app.use((req, res, next) => {
  // ... 其他逻辑
  res.header('Access-Control-Allow-Headers', ALLOWED_HEADERS.join(', '));
});

虽然不够完美,但至少比 * 安全,也方便团队协作。当然,上线前我会清理掉临时 header,比如 X-Debug-Flag

结尾提醒

最后再强调一遍:别信“加个 * 就行”的鬼话,尤其是在涉及用户凭证的场景。CORS 的设计本意就是安全,绕过它等于给自己埋雷。我宁愿多花两分钟配清楚 header,也不想半夜被线上跨域问题叫醒。

以上是我踩坑后的总结,希望对你有帮助。有更好的方案欢迎评论区交流,比如你们怎么管理动态 header 白名单?或者有没有自动化工具能同步前后端的 header 配置?

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

暂无评论