写点什么

掌握 CORS 跨域请求,读这一篇文章就够了

作者:范家鹏
  • 2022-12-08
    北京
  • 本文字数:4686 字

    阅读完需:约 15 分钟

掌握 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 字段由三部分组成:协议、主机和端口,以下三种语法都是正确的。

origin: nullorigin: <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 示例说明:

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/simpleorigin: https://tool.box3.cnuser-agent: Mozilla/5.0 ... ...
复制代码

以下是服务器返回的响应报文(关键部分信息):

:status: 200 OKserver: nginxdate: Thu, 17 Nov 2022 02:35:49 GMTcontent-type: application/json; charset=utf-8content-length: 47access-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/requestaccess-control-request-method: GETaccess-control-request-headers: box3-tokenorigin: https://tool.box3.cnuser-agent: Mozilla/5.0 ... ...
/* 预检响应头 */:status: 204 No Contentserver: nginxdate: Thu, 17 Nov 2022 02:35:35 GMTaccess-control-allow-headers: box3-tokenaccess-control-allow-origin: *
复制代码

access-control-request-headers 告知服务器实际请求携带的自定义标头,access-control-allow-headers 告知客户端已支持的所有自定义标头,多个值之间以逗号分隔。


一般而言,服务器会对 OPTIONS 请求的结果添加缓存时间。目的是,客户端减少了预检请求交互的时间,同时也减少了对服务器的压力。比如服务器在响应头中指定 access-control-max-age: 3600 表示该响应的有效时间为 3600 秒,也就是 1 小时。在这段时间内,浏览器不会对同一请求再次发起预检请求,而是直接发起实际情况。


添加预检请求缓存之后,本例的预检响应头,最新内容如下:

:status: 204 No Contentserver: nginxdate: Thu, 17 Nov 2022 02:35:35 GMTaccess-control-allow-headers: box3-tokenaccess-control-allow-origin: *access-control-max-age: 3600
复制代码

关键知识点:对于 OPTIONS 请求,合法的 HTTP 状态码,应该定义在 2xx 范围内。比如状态码设置为 200 或 204,都是正确的。


最后,待预检请求通过之后,浏览器再发送实际请求。下面是实际请求的请求头和响应头:

/* 实际请求的请求头 */:method: GET:authority: api.box3.cn:scheme: https:path: /example/requestbox3-token: 111-222-333-444origin: https://tool.box3.cnuser-agent: Mozilla/5.0 ... ...
/* 实际请求的响应头 */:status: 200 OKserver: nginxdate: Thu, 17 Nov 2022 02:35:35 GMTcontent-type: application/json; charset=utf-8content-length: 45access-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: httpscookie: access-token=100;origin: https://tool.box3.cnuser-agent: Mozilla/5.0 ... ...
/* 简单请求的响应头 */:status: 200 OKserver: nginxdate: Thu, 17 Nov 2022 02:52:07 GMTcontent-type: application/json; charset=utf-8content-length: 45access-control-allow-credentials: trueaccess-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 请求头字段
7.2 HTTP 响应头字段


发布于: 33 分钟前阅读数: 12
用户头像

范家鹏

关注

工欲善其事,必先利其器。 2020-08-01 加入

https://www.box3.cn

评论

发布
暂无评论
掌握 CORS 跨域请求,读这一篇文章就够了_HTTP_范家鹏_InfoQ写作社区