|
在 Web 前后端分离架构模式下,跨域(跨源)请求属于日常的基本情况了。浏览器出于安全考虑,会限制 JavaScript(简称 JS)脚本内发起跨源 HTTP 请求,同源没有此类限制。前端解决跨域方法有很多,比如WebSocket 协议跨域、JSONP 请求跨域和跨域资源共享 CORS等。01CORS 简介CORS 全称为 Cross-Origin Resource Sharing,被译为跨域资源共享,简称跨域访问,是 W3C 制定的标准协议。它由一系列传输的 HTTP 标头(首部字段)组成,浏览器会根据这些 HTTP 标头决定着是否阻止前端 JS 代码获取跨域请求的资源。CORS 主要作用是消除各种 API 的同源限制,以便在不同源(服务器)之间共享资源,且确保跨域数据传输的安全性。CORS 请求并不是一种特殊的 HTTP 请求,同样基于 HTTP 通信协议。CORS 请求默认携带"origin"标头,用于向目标网站指明请求的来源。origin 字段由三部分组成:协议、主机和端口,以下三种语法都是正确的。origin: nullorigin: ://origin: ://:
02查询浏览器的兼容性推荐一个查询浏览器特性、兼容性以及兼容到具体哪个版本的网站。例如查询各浏览器对 CORS 的支持情况,访问 URL 地址 https://caniuse.com/search=CORS。如下图所示:03同源与不同源的定义及举例说明同源策略是由 Netscape 提出的一个著名的安全策略,它是一种安全约定。目前,所有可支持 JS 的浏览器都会遵循这个策略。Ajax 是当代 Web 应用程序中获取服务器数据的核心技术,可以实现网页内容异步更新,Ajax 底层之 XMLHttpRequest 对象和 Fetch API 都遵循同源策略。同源策略也是浏览器基本的安全功能之一。同源的定义:当两个 URL 使用的协议、域名(主机)和端口都相同的情况下,则称为两个 URL 同源,反之称两个 URL 不同源。下表整理了同源与不同源的 URL 示例说明:URLAURL 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.comhttp://www.example.com:80同源80 是 HTTP 协议默认端口https://www.example.comhttps://www.example.com:443同源443 是 HTTPS 协议默认端口https://www.example.comhttp://www.example.com不同源域名相同,协议不同https://www.example.comhttps://www.example.com:81不同源域名相同,端口不同https://www.example.comhttps://tool.example.com不同源主域名相同,二级域名不同https://www.example.comhttps://example.com不同源主域名相同,子域名不同https://www.example.comhttps://39.105.183.157不同源域名与 IP 不同https://www.example.comhttps://tool.box3.cn不同源完全不同的域名http://www.example.comhttp://localhost不同源完全不同的域名04常见的 CORS 访问控制场景本例中,Nginx 服务器开启了 HTTP/2 协议,因此在 HTTP/2 二进制编码之前,必须将 HTTP 标头名称转换为小写。若请求头、响应头中包含大写的字段名将被视为格式错误。关键知识点:如果 CORS 跨域请求是这三种方法之一:GET、POST 或 HEAD,那么在 HTTP 响应头中并不需要指明 access-control-allow-methods 字段的值。4.1 简单请求什么是简单请求?如果满足下述所有条件,才会被认定为"简单请求"。请注意,对于"简单请求"浏览器不会发起 CORS 预检请求。1、HTTP 请求方法是以下三种之一:GETPOSTHEAD2、除了浏览器自动添加的首部字段(例如:connection,user-agent、date、referer等)和 fetch 规范中定义的禁止使用的首部字段,以及"proxy-"和"sec-"小写开头的首部字段。允许设置的首部字段集合为:acceptaccept-languagecontent-languagecontent-type(见下列 3 )3、content-type 的值是下列三者之一:text/plainmultipart/form-dataapplication/x-www-form-urlencoded4、请求中的任意 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关键知识点:服务器在响应头中必须指定 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、将该请求优化成一个简单请求。05常见的 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 服务器与请求方之间的凭证许可配置错误。06借助浏览器找错误引发 CORS 错误的原因是跨域请求失败导致,并非 JS 代码层面出现的逻辑性 BUG。如果 JS 发起的 HTTP 请求产生 CORS 错误,在 JS 代码层面无法获知具体是哪里出了问题,但是您可通过浏览器控制台获悉错误信息。例如在 Chrome 浏览器中,通过 F12 键启动开发者调试工具,在 Network 面板中了解具体的报错信息。如下图所示:07认识这些 HTTP 请求头和响应头HTTP 请求头字段Header说明origin表明预检请求或实际请求的源站。origin 的值只包括协议、域名、端口,不包含路径和参数。access-control-request-method出现于预检请求中,其作用是,通知服务器在实际请求中采用哪种 HTTP 方法。access-control-request-headers出现于预检请求中,其作用是,通知服务器在实际请求中使用哪些 HTTP 请求头。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 代码。请注意,该值严格区分大小写,正确的写法是全小写。
|
|