◆

前端请求跨域:Access-Control-Allow-Headers 报错解决

2023-04-04 01:230

场景

后端接口设置了跨域请求,但是前端依旧跨域

先说清楚这个报错的来龙去脉,不然很容易改错地方。浏览器发跨域请求时,如果请求不属于「简单请求」(比如带了自定义头、或者 Content-Type 是 application/json),会先自动发一个 OPTIONS 预检请求,把接下来要用的方法和请求头报给服务端,问一句「这些我能发吗」。

预检时浏览器会把所有非简单的请求头名字塞进 Access-Control-Request-Headers。服务端要在响应的 Access-Control-Allow-Headers 里把这些头名字全部列出来,浏览器逐一比对,只要有一个头没被声明,整个预检就失败,真正的请求根本不会发出去。这就是为什么后端明明配了 CORS 却还是跨域——Access-Control-Allow-Origin 是对的,但头的白名单漏了。

如下,使用 express 设置允许跨域请求,但是前端请求依旧跨域,总是提示 Request header field Access-Control-Allow-Headers is not allowed by itself in preflight response, 可以看出来上述的提示表明,请求头依旧是有问题的。

app.use((req, res, next) => {
  console.log(req.headers);
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', 'authorization,content-type');
  res.header(
    'Access-Control-Allow-Methods',
    'GET, POST, OPTIONS, PATCH, PUT, DELETE',
  );
  next();
});

上面这段配置只声明了 authorization 和 content-type 两个头,本地开发时够用,一上线就挂了。原因是线上的请求链路里多出了一个头。

解决方法

将请求时的请求头信息打印出来找到 Access-Control-Allow-Headers 对应的字段,添加上去,我所遇到的情况就是, 在本地请求时没有 client 这个字符串,而升级到线上请求头中有这个字段,添加上去即可

排查手段就是那句 console.log(req.headers)。预检的 OPTIONS 请求也会走到这个中间件,打印出来能直接看到 access-control-request-headers 的值,浏览器要求放行哪些头一目了然,照着补进白名单就行。不用猜,也不用去翻前端代码找是谁加的头。

至于为什么线上多了个 client:这类头通常不是业务代码显式设置的,而是网关、SDK 或者 axios 拦截器统一注入的,本地跑的时候那一层不在链路上,所以没出现。类似的还有埋点 SDK 加的 trace 头、灰度系统加的版本头。

app.use((req, res, next) => {
  console.log(req.headers);
  res.header('Access-Control-Allow-Origin', '*');
  res.header(
    'Access-Control-Allow-Headers',
    'client,authorization,content-type',
  );
  res.header(
    'Access-Control-Allow-Methods',
    'GET, POST, OPTIONS, PATCH, PUT, DELETE',
  );
  next();
});

几个容易踩的点

头名字大小写不敏感,但不能随便用 * 代替。HTTP 头名本身大小写不敏感,Authorization 和 authorization 等价。Access-Control-Allow-Headers 虽然规范上支持 *,但当请求带凭证(credentials: 'include')时 * 会失效,必须逐个列出来。同理,带凭证的请求里 Access-Control-Allow-Origin 也不能是 *,得回显具体的 origin 并加上 Access-Control-Allow-Credentials: true。

预检本身要有正确的响应。上面的中间件只 next() 没有拦截 OPTIONS,如果后面的路由没注册 OPTIONS 方法,express 会返回 404,预检同样算失败。稳妥的做法是显式处理:

if (req.method === 'OPTIONS') return res.sendStatus(204);

加上 Access-Control-Max-Age 减少预检次数。默认每个跨域请求前都要预检一次,等于请求量翻倍。设置这个头(单位秒)让浏览器缓存预检结果,比如 res.header('Access-Control-Max-Age', '86400'),同一个接口一段时间内只预检一次。注意各浏览器有自己的上限,Chrome 会把超过 2 小时的值截断。

前端读不到自定义响应头是另一回事。如果需要在前端拿到后端返回的自定义头(比如分页总数),要额外配 Access-Control-Expose-Headers,否则 JS 里只能读到那几个默认暴露的头。这个和 Allow-Headers 管的是两个方向,别搞混。

💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVE前端请求跨域:Access-Control-Allow-Headers 报错解决加载中