js跨域请求获取不到自定义响应头的问题

Can Not Get Response Header using JavaScript in CORS Request

近期做一个前后端分离的项目,前端通过跨域请求访问后端,后端的返回结果中有一个自定义的响应头。但是在浏览器中,前端却无法获取到这个响应头:

问题场景

我们尝试分别用jQuery ajax、XMLHttpRequest、fetch三种方式来发送一个 CORS 跨域请求,并打印响应头:

const backEndUrl = 'http://127.0.0.1:8888'; console.log('前端URL:' + location.href); console.log('后端URL:' + backEndUrl); // jQuery ajax $.get(backEndUrl, (data, status, xhr) => { console.log('用jQuery的ajax发送请求'); console.log('响应头:\n' + xhr.getAllResponseHeaders()); }); // XMLHttpRequest const xhr = new XMLHttpRequest(); xhr.open('get', backEndUrl); xhr.send(); xhr.onload = () => { console.log('用XMLHttpRequest发送请求'); console.log('响应头:\n' + xhr.getAllResponseHeaders()); }; // fetch fetch(backEndUrl).then((response) => { const array = []; response.headers.forEach((value, name) => { array.push(name + ': ' + value); }); console.log('用fetch发送请求'); console.log('响应头:\n' + array.join('\n')); });

然后在Chrome浏览器的开发者工具中查看这个跨域请求的请求头和响应头:

带有自定义响应头的跨域请求

其中可以看到,前端页面的端口是 8848,后端服务的端口是 8888,不同源,所以这是一个 CORS 跨域请求。另外,后端服务返回了一个自定义的响应头:

My-Personal-Website: https://qzy.im

但是,当我们查看控制台时,却发现只打印了以下内容:

跨域请求时控制台打印的内容

在众多的响应头中,只有 content-type 可以通过js获取到,其他响应头(包括自定义的响应头)都无法获取。

如果我们将前端页面的端口改成 8888,和后端同源,重新发送一个同源请求,这种情况控制台打印如下:

非跨域请求时控制台打印的内容

可见,这三种方式在不跨域时,js都可以获取到完整的响应头(除了 Set-Cookie)。

原因分析

在测试了Chrome、FireFox、Safari、Edge等多个主流浏览器之后,我们发现 CORS 跨域请求的响应头都无法用js完整获取。因此,应该是浏览器对这一功能做了限制。

由于这些主流浏览器都遵循WHATWG制定的Web标准,因此我们追根溯源,在最新版本的Web请求标准规范中,找到了以下内容:

A CORS-safelisted response-header name, given a CORS-exposed header-name list list, is a header name that is a byte-case-insensitive match for one of

默认情况下,只有以上7个响应头是CORS安全的(早期有6个,Content-Length 是后面加上的),因此浏览器也只允许js获取这7个响应头。除此之外,规范中还有这样的说明:

A response has an associated CORS-exposed header-name list (a list of zero or more header names). The list is empty unless otherwise specified.

A response will typically get its CORS-exposed header-name list set by extracting header values from the Access-Control-Expose-Headers header. This list is used by a CORS filtered response to determine which headers to expose.

规范表明每个响应都有一个CORS公开响应头列表,这个列表默认为空,服务器可以通过添加 Access-Control-Expose-Headers 响应头来指定哪些响应头是可以公开的。

因此,浏览器在发送 CORS 跨域请求时,js只能获取到7个CORS安全的响应头、以及 Access-Control-Expose-Headers 中指定的响应头。

以上标准在MDN Web文档中也可以查看,相关链接:

解决办法

原因已经找到,我们只需要按照规范文档,在后端添加 Access-Control-Expose-Headers 响应头即可。这个响应头的语法为:

Access-Control-Expose-Headers: <header-name>, <header-name>, ... Access-Control-Expose-Headers: *

使用通配符 * 可以公开所有响应头,但是并不安全。因此我们可以只把部分响应头公开给前端,多个响应头名称之间用 , 分隔。

Java中添加响应头的方法:

HttpServletResponse response;// HttpServletResponse实例 response.setHeader("Access-Control-Expose-Headers", "My-Personal-Website");

Nginx中添加响应头的方法:

add_header Access-Control-Expose-Headers "My-Personal-Website";

添加上 Access-Control-Expose-Headers 响应头之后,我们再发送一个 CORS 跨域请求,查看控制台打印的内容:

添加Access-Control-Expose-Headers响应头之后,跨域请求时控制台打印的内容

这样,我们终于能在跨域请求时,用js获取到我们想要的响应头了。

 

文章评论
${fromAuthor ? '郄正元' : '游客'} 作者 ${gmtCreate}
${content}
${subList.length}
发表评论
${commentToArticle ? '' : parentContent}
字数:0/${maxCommentLength}
该邮箱地址仅用于接收其他用户的回复提醒,不会泄露