掌握 CORS 跨域请求,读这一篇文章就够了
在 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 字段由三部分组成:协议、主机和端口,以下三种语法都是正确的。
2、查询浏览器的兼容性
推荐一个查询浏览器特性、兼容性以及兼容到具体哪个版本的网站。例如查询各浏览器对 CORS 的支持情况,访问 URL 地址 https://caniuse.com/?search=CORS。如下图所示:
3、同源与不同源的定义及举例说明
同源策略是由 Netscape 提出的一个著名的安全策略,它是一种安全约定。目前,所有可支持 JS 的浏览器都会遵循这个策略。Ajax 是当代 Web 应用程序中获取服务器数据的核心技术,可以实现网页内容异步更新,Ajax 底层之 XMLHttpRequest 对象和 Fetch API 都遵循同源策略。同源策略也是浏览器基本的安全功能之一。
同源的定义:当两个 URL 使用的协议、域名(主机)和端口都相同的情况下,则称为两个 URL 同源,反之称两个 URL 不同源。下表整理了同源与不同源的 URL 示例说明:
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 代码如下所示:
以下是浏览器发送给服务器的请求报文(关键部分信息):
以下是服务器返回的响应报文(关键部分信息):
本例中,服务器返回的首部字段 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 代码示例:
如上代码使用 GET 请求从服务器获取数据,该请求包含了一个自定义的请求头(box3-token:111-222-333-444)。因为该字段名超出了"简单请求"的定义范围,所以浏览器自行判断出这是一个非简单请求,在"实际请求"发起之前,会先发起一个"预检请求"。
下面是浏览器与服务器首次交互的报文信息,包括预检请求头和预检响应头(备注:user-agent 省略了部分内容):
access-control-request-headers 告知服务器实际请求携带的自定义标头,access-control-allow-headers 告知客户端已支持的所有自定义标头,多个值之间以逗号分隔。
一般而言,服务器会对 OPTIONS 请求的结果添加缓存时间。目的是,客户端减少了预检请求交互的时间,同时也减少了对服务器的压力。比如服务器在响应头中指定 access-control-max-age: 3600 表示该响应的有效时间为 3600 秒,也就是 1 小时。在这段时间内,浏览器不会对同一请求再次发起预检请求,而是直接发起实际情况。
添加预检请求缓存之后,本例的预检响应头,最新内容如下:
关键知识点:对于 OPTIONS 请求,合法的 HTTP 状态码,应该定义在 2xx 范围内。比如状态码设置为 200 或 204,都是正确的。
最后,待预检请求通过之后,浏览器再发送实际请求。下面是实际请求的请求头和响应头:
4.3 简单请求和凭据
默认情况下,对于 XMLHttpRequest 或 Fetch API 发起的跨域请求,浏览器不会发送 Cookie 信息。若要携带 Cookie,以 XMLHttpRequest 对象为例,需要设置属性 withCredentials 的值为 true。
本例中,站点 https://tool.box3.cn 内的 JS 脚本向 https://api.box3.cn 发起了一个简单的 GET 跨域请求,并附带了身份凭证 Cookie。JS 示例代码如下:
下面是浏览器与服务器交互的报文信息之关键部分(备注:user-agent 省略了部分内容):
关键知识点:
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,大多数浏览器将报告如下错误:
有两种方式可以规避上述报错行为:
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 请求头字段
7.2 HTTP 响应头字段
版权声明: 本文为 InfoQ 作者【范家鹏】的原创文章。
原文链接:【http://xie.infoq.cn/article/a5155db079376f4378220a6ca】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论