彻底搞懂 Access-Control-Allow-Headers 的作用与实战配置技巧

设计师婧妍 安全 阅读 2,816
赞 14 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

上个月我们上线了一个新功能,前端要调用后端的几个新接口,结果用户反馈页面加载慢得离谱。我本地测试还好,但一上生产环境,首屏数据拉取动不动就5秒起步,有时候甚至直接超时。

彻底搞懂 Access-Control-Allow-Headers 的作用与实战配置技巧

一开始我以为是网络问题,或者后端接口太慢。但查了监控,接口响应时间其实就200ms左右,完全不应该是瓶颈。那问题出在哪?后来发现,每次请求都卡在“pending”状态好几秒,然后才真正发出——这明显是CORS预检(preflight)的问题。

没错,就是那个烦人的 OPTIONS 请求。因为我们在请求里加了自定义 header,比如 X-Client-VersionX-Request-ID 这些,浏览器就会先发一个 OPTIONS 请求去问服务器:“你允许我带这些 header 吗?”如果服务器没配好,或者配置太宽泛,这个过程就会拖慢整个请求链路。

找到瓶颈了!

我打开 Chrome DevTools 的 Network 面板,把 “Disable cache” 勾上,刷新页面,果然看到一堆 OPTIONS 请求,每个都耗时 800ms 到 1.5s 不等。点进去看,Response Headers 里 Access-Control-Allow-Headers 的值是 *

这里有个坑:很多人以为设成 * 就万事大吉了,其实不是。根据 CORS 规范,当请求包含 credentials(比如 cookies)或者使用了自定义 header 时,Access-Control-Allow-Headers 不能用通配符 *,必须显式列出允许的 header 名称。否则浏览器会直接拒绝后续的正式请求,或者在某些环境下反复重试,导致延迟。

更糟的是,我们后端用的是 Nginx + Node.js 架构,Nginx 层和应用层都配了 CORS,但配置不一致,导致 OPTIONS 请求被处理了两次,还返回了不同的 header,进一步加剧了混乱。

核心代码就这几行

折腾了半天,最后发现解决方案其实很简单:**明确列出所有需要的 header,别偷懒用 ***。

先看优化前的后端(Node.js + Express)配置:

app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'https://jztheme.com');
  res.header('Access-Control-Allow-Credentials', 'true');
  res.header('Access-Control-Allow-Headers', '*'); // ❌ 问题根源
  res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
  next();
});

再看优化后的版本:

const allowedHeaders = [
  'Content-Type',
  'Authorization',
  'X-Client-Version',
  'X-Request-ID',
  'X-Trace-ID'
].join(', ');

app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'https://jztheme.com');
  res.header('Access-Control-Allow-Credentials', 'true');
  res.header('Access-Control-Allow-Headers', allowedHeaders); // ✅ 明确列出
  res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
  next();
});

如果你用的是 Nginx,也得同步改一下。优化前的 Nginx 配置可能长这样:

add_header 'Access-Control-Allow-Headers' '*';

改成:

add_header 'Access-Control-Allow-Headers' 'Content-Type,Authorization,X-Client-Version,X-Request-ID,X-Trace-ID';

这里注意我踩过好几次坑:Nginx 的 add_header 指令只在当前层级生效,如果在 location 块里写了,父级的 header 就不会继承。所以 OPTIONS 请求的处理要单独写清楚,最好统一放在一个 include 文件里,避免重复。

踩坑提醒:这三点一定注意

  • 大小写敏感:header 名称在规范里是不区分大小写的,但有些旧版浏览器或代理服务器会严格匹配。保险起见,前后端保持一致,比如全用小写或首字母大写(推荐用标准写法,如 Content-Type)。
  • 不要漏掉 Content-Type:如果你发的是 JSON 请求,Content-Type: application/json 是必须的,但很多人只记得自定义 header,忘了这个基础项,导致预检失败。
  • 缓存 OPTIONS 响应:即使配对了,每次请求都发 OPTIONS 也是浪费。可以在后端加上 Access-Control-Max-Age,比如设为 86400(一天),让浏览器缓存预检结果。示例:
res.header('Access-Control-Max-Age', '86400');

不过注意,Chrome 对 Max-Age 的最大值有限制(好像是 10 分钟?),超过会被忽略,但设个 600(10分钟)也比没有强。

优化后:流畅多了

改完之后,重新部署,刷新页面——那些烦人的 OPTIONS 请求还在,但每个都变成 10ms 以内了,而且同一个接口后续请求直接跳过预检,直接发正式请求。

最关键的是,首屏数据加载时间从平均 5.2 秒降到了 800ms 左右。虽然接口本身没变快,但省去了多次无效的预检往返,整体体验提升非常明显。

顺便提一句,这个优化对移动端尤其重要。弱网环境下,一次额外的 HTTP 往返可能就是 1~2 秒的延迟,用户感知极强。

性能数据对比

我在 staging 环境做了 A/B 测试,用 Puppeteer 模拟 3G 网络,连续跑 20 次首屏加载:

  • 优化前:平均 5120ms,P95 6200ms,有 3 次超时(>10s)
  • 优化后:平均 780ms,P95 920ms,0 次超时

提升超过 85%。虽然这不是 CPU 或内存优化,但对用户体验的影响一点不比那些“硬核”优化小。

另外,服务器的 OPTIONS 请求量下降了 90% 以上,后端日志干净多了,运维也省心。

结尾

以上是我踩坑后的总结,希望对你有帮助。其实 CORS 本身不是性能问题,但配置不当就会变成性能杀手。核心就一句话:别用 *,明确列出你需要的 header,再加个 Max-Age 缓存预检结果,基本就稳了。

这个方案不是理论最优(比如动态生成 allow headers 会更灵活),但对我们这种中小项目来说,最简单、最可靠。改完后仍有一两个冷门接口偶尔触发预检,但无大碍,不影响主流程。

以上是我个人对 Access-Control-Allow-Headers 的优化经验,有更优的实现方式欢迎评论区交流。这类“小配置大影响”的坑还有很多,后续会继续分享这类博客。

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

暂无评论