在 Web 前后端分离架构模式下,跨域(跨源)请求属于日常的基本情况了。浏览器出于安全考虑,会限制 JavaScript(简称 JS)脚本内发起跨源 HTTP 请求,同源没有此类限制。前端解决跨域方法有很多,比如 WebSocket 协议跨域、JSONP 请求跨域和跨域资源共享 CORS 等。
1、CORS 简介
CORS 全称为 Cross-Origin Resource Sharing,被译为跨域资源共享,简称跨域访问,是 W3C 制定的标准协议。它由一系列传输的 HTTP 标头(首部字段)组成,浏览器会根据这些 HTTP 标头决定着是否阻止前端 JS 代码获取跨域请求的资源。CORS 主要作用是消除各种 API 的同源限制,以便在不同源(服务器)之间共享资源,且确保跨域数据传输的安全性。
CORS 请求并不是一种特殊的 HTTP 请求,同样基于 HTTP 通信协议。CORS 请求默认携带"origin"标头,用于向目标网站指明请求的来源。origin 字段由三部分组成:协议、主机和端口,以下三种语法都是正确的。
origin: null
origin: <scheme>://<hostname>
origin: <scheme>://<hostname>:<port>
2、查询浏览器的兼容性
推荐一个查询浏览器特性、兼容性以及兼容到具体哪个版本的网站。例如查询各浏览器对 CORS 的支持情况,访问 URL 地址 https://caniuse.com/?search=CORS。如下图所示:
3、同源与不同源的定义及举例说明
同源策略是由 Netscape 提出的一个著名的安全策略,它是一种安全约定。目前,所有可支持 JS 的浏览器都会遵循这个策略。Ajax 是当代 Web 应用程序中获取服务器数据的核心技术,可以实现网页内容异步更新,Ajax 底层之 XMLHttpRequest 对象和 Fetch API 都遵循同源策略。同源策略也是浏览器基本的安全功能之一。
同源的定义:当两个 URL 使用的协议、域名(主机)和端口都相同的情况下,则称为两个 URL 同源,反之称两个 URL 不同源。下表整理了同源与不同源的 URL 示例说明:
URL A | URL B | 结果 | 分析原因 |
---|---|---|---|
https://www.example.com/a/ | https://www.example.com/b/ | 同源 | 域名相同,只有路径不同 |
https://www.example.com/a/ | https://www.example.com/a/c/ | 同源 | 域名相同,只有路径不同 |
http://www.example.com | http://www.example.com:80 | 同源 | 80 是 HTTP 协议默认端口 |
https://www.example.com | https://www.example.com:443 | 同源 | 443 是 HTTPS 协议默认端口 |
https://www.example.com | http://www.example.com | 不同源 | 域名相同,协议不同 |
https://www.example.com | https://www.example.com:81 | 不同源 | 域名相同,端口不同 |
https://www.example.com | https://tool.example.com | 不同源 | 主域名相同,二级域名不同 |
https://www.example.com | https://example.com | 不同源 | 主域名相同,子域名不同 |
https://www.example.com | https://39.105.183.157 | 不同源 | 域名与 IP 不同 |
https://www.example.com | https://tool.box3.cn | 不同源 | 完全不同的域名 |
http://www.example.com | http://localhost | 不同源 | 完全不同的域名 |
4、常见的 CORS 访问控制场景
本例中,Nginx 服务器开启了 HTTP/2 协议,因此在 HTTP/2 二进制编码之前,必须将 HTTP 标头名称转换为小写。若请求头、响应头中包含大写的字段名将被视为格式错误。
关键知识点:如果 CORS 跨域请求是这三种方法之一:GET、POST 或 HEAD,那么在 HTTP 响应头中并不需要指明 access-control-allow-methods 字段的值。
4.1 简单请求
什么是简单请求?如果满足下述所有条件,才会被认定为"简单请求"。请注意,对于"简单请求"浏览器不会发起 CORS 预检请求。
1、HTTP 请求方法是以下三种之一:
- GET
- POST
- HEAD
2、除了浏览器自动添加的首部字段(例如:connection,user-agent、date、referer 等)和 fetch 规范中定义的禁止使用的首部字段,以及"proxy-"和"sec-"小写开头的首部字段。允许设置的首部字段集合为:
- accept
- accept-language
- content-language
- content-type(见下列 3 )
3、content-type 的值是下列三者之一:
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
4、请求中的任意 XMLHttpRequest 对象均没有注册任何事件监听器;XMLHttpRequest 对象可以使用 XMLHttpRequest.upload 属性访问。
5、请求中没有使用 ReadableStream 对象。
例如,请看一个 CORS 简单请求的例子,用户访问站点 https://tool.box3.cn,页面尝试跨域请求从 https://api.box3.cn 获取数据,发起跨域请求的 JS 代码如下所示:
const xhr = new XMLHttpRequest();
const url = 'https://api.box3.cn/example/simple';
xhr.open('GET', url);
xhr.send();
以下是浏览器发送给服务器的请求报文(关键部分信息):
:method: GET
:authority: api.box3.cn
:scheme: https
:path: /example/simple
origin: https://tool.box3.cn
user-agent: Mozilla/5.0 ... ...
以下是服务器返回的响应报文(关键部分信息):
:status: 200 OK
server: nginx
date: Thu, 17 Nov 2022 02:35:49 GMT
content-type: application/json; charset=utf-8
content-length: 47
access-control-allow-origin: *
本例中,服务器返回的首部字段 access-control-allow-origin: * 表明,该资源可以被任意外部域访问或接受所有的请求源。
access-control-allow-origin: *
如果只希望服务器允许来自 https://www.example.com 的访问,该首部字段的内容如下:
access-control-allow-origin: https://www.example.com
关键知识点:当响应的是附带身份凭证的请求时(例如:Cookie),服务器必须明确 access-control-allow-origin 字段的值,而不能使用通配符"*",否则浏览器的同源策略会阻止该请求,并在控制台抛出错误。
4.2 预检请求和实际请求
首先,当请求发生跨域行为,且非简单请求时,才会产生 CORS 预检请求(CORS-preflight request)。其次与"简单请求"不同的是,"预检请求"是由浏览器自动发起的一个额外的 OPTIONS 请求,以获知服务器是否授权后续的实际请求(例如:XHR 或 Fetch API 发起的 HTTP 跨域请求)。其次,OPTIONS 请求包含了两个重要的标头(首部字段)access-control-request-method 和 access-control-request-headers。
如下是一段需要发起 HTTP 预检请求的 JS 代码示例:
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.box3.cn/example/request');
xhr.setRequestHeader('box3-token', '111-222-333-444');
xhr.send();
如上代码使用 GET 请求从服务器获取数据,该请求包含了一个自定义的请求头(box3-token:111-222-333-444)。因为该字段名超出了"简单请求"的定义范围,所以浏览器自行判断出这是一个非简单请求,在"实际请求"发起之前,会先发起一个"预检请求"。
下面是浏览器与服务器首次交互的报文信息,包括预检请求头和预检响应头(备注:user-agent 省略了部分内容):
/* 预检请求头 */
:method: OPTIONS
:authority: api.box3.cn
:scheme: https
:path: /example/request
access-control-request-method: GET
access-control-request-headers: box3-token
origin: https://tool.box3.cn
user-agent: Mozilla/5.0 ... ...
/* 预检响应头 */
:status: 204 No Content
server: nginx
date: Thu, 17 Nov 2022 02:35:35 GMT
access-control-allow-headers: box3-token
access-control-allow-origin: *
access-control-request-headers 告知服务器实际请求携带的自定义标头,access-control-allow-headers 告知客户端已支持的所有自定义标头,多个值之间以逗号分隔。
一般而言,服务器会对 OPTIONS 请求的结果添加缓存时间。目的是,客户端减少了预检请求交互的时间,同时也减少了对服务器的压力。比如服务器在响应头中指定 access-control-max-age: 3600 表示该响应的有效时间为 3600 秒,也就是 1 小时。在这段时间内,浏览器不会对同一请求再次发起预检请求,而是直接发起实际情况。
添加预检请求缓存之后,本例的预检响应头,最新内容如下:
:status: 204 No Content
server: nginx
date: Thu, 17 Nov 2022 02:35:35 GMT
access-control-allow-headers: box3-token
access-control-allow-origin: *
access-control-max-age: 3600
关键知识点:对于 OPTIONS 请求,合法的 HTTP 状态码,应该定义在 2xx 范围内。比如状态码设置为 200 或 204,都是正确的。
最后,待预检请求通过之后,浏览器再发送实际请求。下面是实际请求的请求头和响应头:
/* 实际请求的请求头 */
:method: GET
:authority: api.box3.cn
:scheme: https
:path: /example/request
box3-token: 111-222-333-444
origin: https://tool.box3.cn
user-agent: Mozilla/5.0 ... ...
/* 实际请求的响应头 */
:status: 200 OK
server: nginx
date: Thu, 17 Nov 2022 02:35:35 GMT
content-type: application/json; charset=utf-8
content-length: 45
access-control-allow-origin: *
4.3 简单请求和凭据
默认情况下,对于 XMLHttpRequest 或 Fetch API 发起的跨域请求,浏览器不会发送 Cookie 信息。若要携带 Cookie,以 XMLHttpRequest 对象为例,需要设置属性 withCredentials 的值为 true。
本例中,站点 https://tool.box3.cn 内的 JS 脚本向 https://api.box3.cn 发起了一个简单的 GET 跨域请求,并附带了身份凭证 Cookie。JS 示例代码如下:
const xhr = new XMLHttpRequest();
const url = 'https://api.box3.cn/example/simple_cookie';
xhr.open('GET', url);
xhr.withCredentials = true;
xhr.send();
下面是浏览器与服务器交互的报文信息之关键部分(备注:user-agent 省略了部分内容):
/* 简单请求的请求头 */
:method: GET
:authority: api.box3.cn
:path: /example/simple_cookie
:scheme: https
cookie: access-token=100;
origin: https://tool.box3.cn
user-agent: Mozilla/5.0 ... ...
/* 简单请求的响应头 */
:status: 200 OK
server: nginx
date: Thu, 17 Nov 2022 02:52:07 GMT
content-type: application/json; charset=utf-8
content-length: 45
access-control-allow-credentials: true
access-control-allow-origin: https://tool.box3.cn
关键知识点:
1、服务器在响应头中必须指定 access-control-allow-credentials: true 来表明跨域请求允许携带 Cookie,否则仍然会被浏览器的 CORS 策略阻止。
2、服务器在响应头中必须指定 access-control-allow-origin 字段特定的域,该标头的值不能设置为通配符 "*",否则仍然会被浏览器的 CORS 策略阻止。
4.4 预检请求和凭据
首先,一个完整的 CORS 预检请求,是由浏览器自动完成的,这个动作对用户是无感知的。
其次,与"简单请求和凭据"这小节整理的 CORS 策略知识点是一致的。那意味着,在 OPTIONS 请求的响应头中必须明确指定 access-control-allow-credentials: true 和 access-control-allow-origin 字段特定的域,否则后续的实际请求仍然会被浏览器的 CORS 策略阻止。
最后,在实际请求的响应头中,也需要明确指定这两个字段且保持与 OPTIONS 相同的值。
关键知识点:如果实际请求的 HTTP 方法,非 GET、POST 或 HEAD,那么 access-control-allow-methods 字段的值不能设置为通配符"*",应设置为特定的 HTTP 请求方法名称,多个值之间以逗号分隔。
4.5 预检请求与重定向
回顾 4.2 小节的关键知识点,预检请求指的是 OPTIONS 请求,且 HTTP 状态码定义在 2xx 范围内。因此,如果一个预检请求发生了重定向,那么 HTTP 状态码一定大于 2xx,大多数浏览器将报告如下错误:
Access to XMLHttpRequest at 'https://api.box3.cn/example/request_redirect' from origin 'https://tool.box3.cn' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.
有两种方式可以规避上述报错行为:
1、在服务端上去掉对预检请求的重定向。
2、将该请求优化成一个简单请求。
5、常见的 4 种 CORS 错误
常见的 CORS 跨域请求错误,可能有以下 4 种情况(以下首部字段在服务器上配置):
1、受信来源 access-control-allow-origin 配置不正确。
2、受信的 HTTP 方法 access-control-allow-methods 配置不全。
3、受信的首部字段 access-control-allow-headers 配置不全。
4、access-control-allow-credentials 服务器与请求方之间的凭证许可配置错误。
6、借助浏览器找错误
引发 CORS 错误的原因是跨域请求失败导致,并非 JS 代码层面出现的逻辑性 BUG。如果 JS 发起的 HTTP 请求产生 CORS 错误,在 JS 代码层面无法获知具体是哪里出了问题,但是您可通过浏览器控制台获悉错误信息。例如在 Chrome 浏览器中,通过 F12 键启动开发者调试工具,在 Network 面板中了解具体的报错信息。如下图所示:
7、认识这些 HTTP 请求头和响应头
7.1 HTTP 请求头字段
Header | 说明 |
---|---|
origin | 表明预检请求或实际请求的源站。origin 的值只包括协议、域名、端口,不包含路径和参数。 |
access-control-request-method | 出现于预检请求中,其作用是,通知服务器在实际请求中采用哪种 HTTP 方法。 |
access-control-request-headers | 出现于预检请求中,其作用是,通知服务器在实际请求中使用哪些 HTTP 请求头。 |
7.2 HTTP 响应头字段
Header | 说明 |
---|---|
access-control-allow-origin | 指定请求的资源能共享给哪些域。该字段只能指定一个来源。对于不需要携带身份凭证的请求,可以设置为通配符 *,表示允许所有来源访问。 |
access-control-expose-headers | 在跨源访问时,XMLHttpRequest 对象的 getResponseHeader() 方法只能拿到一些最基本的响应头。如果需要获取其他响应头,通过该字段添加白名单。 |
access-control-allow-methods | 对于预检请求的响应,指明实际请求允许使用哪些 HTTP 方法。 |
access-control-allow-headers | 对于预检请求的响应,指明实际请求允许携带哪些 HTTP 头。 |
access-control-max-age | 指定预检请求的有效期,单位是秒。目的是减少发起预检请求的次数。 |
access-control-allow-credentials | 当设置为 true 时,告诉浏览器将响应公开给前端 JavaScript 代码。请注意,该值严格区分大小写,正确的写法是全小写。 |
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。