我所知道的HTML——跨源资源共享(CORS)结合真实面试题,深入浅出为大家介绍跨源资源共享(CORS),帮助大家理解理
往期回顾
(如果您正巧因为首页推荐的功能点进此文章,由衷地建议您先回顾往期内容,这将有助您接下来的阅读体验。)
前言
在上一篇文章中,我们讨论了Web Worker
和Window.postMessage
,学习了通过多线程处理优化页面性能以及跨源通信。并对同源策略是什么和有什么用做了复习回顾。
在本篇文章中,我们会一起学习最后剩下的这几个新特性:
- 跨源资源共享(CORS): Access-Control-Allow-Origin以及衍生的网络安全问题
- 全双工通信协议: Websocket
- 离线应用:本地存储 localStorage 和 sessionStorage 和 manifest
跨源资源共享(CORS)
跨源资源共享(CORS:Cross-Origin Resource Sharing,或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其他源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的“预检”请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。——MDN
上面的定义文字我们可以分成2部分来看:
- 基于 HTTP 头的机制,使得浏览器允许其他源访问加载自己的资源。
- 通过一种机制来检查服务器是否会允许发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的“预检”请求。
这两部分,是面试题经常考察的知识点,因此在正式进入学习内容之前,我想先列一列它们,大家带着这些问题继续阅读:
- 跨源请求是发出浏览器了还是没有?
- 跨源请求如果想携带cookie,客户端和服务端分别需要做什么?
- 为什么POST请求会发送两次?
- 大量的预检请求严重影响网络通信的速度,如何避免?
👇我们先来看一张CORS机制的示意图,由于MDN中文文档上图片挂了,因此我自己画了一张👇:
虽然我们的工作不同,做的业务也不同,但是往往我们都会遇到页面里出现跨源请求的情况,举个例子,如果你的网站具备滑块验证的话,滑块的图片可能就是从第三方网站加载的,那么很显然,第三方网站和你的网站是不同源的。
我在这个个人项目里使用的滑块验证组件,内部逻辑是请求picsum这个网站来获取图片资源的。
所以实际上,CORS并不是单纯的八股文,学习它、掌握它、对我们的工作是真的会有帮助。
当然,上面只是我结合自己的个人项目举的一个小例子,一起来看看CORS的应用场景吧。
什么情况下会需要CORS?
- 前文提到的由
XMLHttpRequest
或 Fetch API 发起的跨源 HTTP 请求。- 这里其实也可以扩展为一些面试题,
XMLHttpRequest
和Fetch API
的区别是什么?- 比如兼容性、使用方式、错误处理等方面的比较。
- 说说
axios
是基于二者中的谁实现的?你能手写一个简单的axios
吗?
当然,这里只是简单的打个样,只满足了function axios({ method, url, data = null }) { return new Promise((resolve, reject) => { // 创建 XMLHttpRequest 对象 const xhr = new XMLHttpRequest(); // 监听状态变化 xhr.onreadystatechange = function () { if (xhr.readyState === 4) { // 请求完成 if (xhr.status >= 200 && xhr.status < 300) { // 请求成功 resolve({ data: JSON.parse(xhr.responseText), status: xhr.status, statusText: xhr.statusText }); } else { // 请求失败 reject(new Error(`Request failed with status code ${xhr.status}`)); } } }; // 处理错误 xhr.onerror = function () { reject(new Error('Network error')); }; // 配置请求 xhr.open(method.toUpperCase(), url, true); // 设置请求头,如果是 POST 请求且发送了数据 if (method.toUpperCase() === 'POST' && data) { xhr.setRequestHeader('Content-Type', 'application/json'); } // 发送请求 xhr.send(data ? JSON.stringify(data) : null); }); } // 使用 axios 发送 GET 请求 axios({ method: 'get', url: 'https://foo.com/todos/1' }).then(response => { console.log('GET Response:', response); }).catch(error => { console.error('GET Error:', error); }); // 使用 axios 发送 POST 请求 axios({ method: 'post', url: 'https://bar.com/register', data: { title: 'foo', body: 'bar', userId: 1 } }).then(response => { console.log('POST Response:', response); }).catch(error => { console.error('POST Error:', error); });
GET
和POST
两种类型的请求。实际上axios库还包含很多功能,如拦截器、取消请求、超时设置、请求和响应转换等,有机会的话,我们在手写系列文章中聊一聊它。
- 这里其实也可以扩展为一些面试题,
- Web 字体(CSS 中通过
@font-face
使用跨源字体资源),因此,网站就可以发布 TrueType 字体资源,并只允许已授权网站进行跨站调用。 - WebGL 贴图。
- 使用
drawImage()
将图片或视频画面绘制到 canvas。 - 来自图像的 CSS 图形。
- 什么意思呢,意思就是通过
url
的方式去配置某个图形的shape-outside
属性时,这个url
可以是cdn加速后的url,因此就要注意跨源的问题了。 - 那其实从这里又可以衍生一些面试题了,为什么要使用cdn加速,它能给哪些类型的资源加速,动态资源可以吗?
- 除了cdn加速之外,优化首屏加载的方式还有哪些呢?
- 为什么
SPA
项目不做这些优化就会加载很慢啊?
- 什么意思呢,意思就是通过
OK,在上一个章节中,我们了解了CORS的使用场景,并且还进行了头脑风暴,对一部分衍生的面试题做了讨论。
接下来,我们聊一聊怎么去完成一次跨源资源共享。
设置正确的HTTP头
回到最开始的定义章节,复习一下定义:
跨源资源共享(CORS:Cross-Origin Resource Sharing,或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其他源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。
因此如果我们期望跨源资源共享的行为能够成功,我们就得设置正确的HTTP头。
这就回到了之前衍生过的第一道面试题了:
跨源请求是发出浏览器了还是没有?
跨源请求确实发出浏览器了,服务器也会响应这个请求。但是返回的时候,由于同源策略的限制,被浏览器拦截了。
我们可以直接在控制台运行一段代码,看下效果:
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://www.baidu.com');
xhr.send(null);
xhr.onload = function(e) {
var xhr = e.target;
console.log(xhr.responseText);
}
我们来分析一下这个过程,通过【网络】tab我们可以查看这次请求发起的头信息:
上图中的Origin
字段说明本次请求的源,服务器会根据这个值,来决定是否同意这次请求。
Host
字段表面请求的目标主机。
可以看到,Origin
和Host
不同,因此这满足跨源的定义,这是个跨源请求。
服务器会响应这个请求,并返回一个正常的HTTP回应,不过浏览器会发现,从服务器返回的HTTP回应的头信息中并没有包含Access-Control-Allow-Origin
字段,
因此浏览器会抛出错误,这个错误会被XMLHttpRequest
的onerror
事件捕获。
而且我们还不能根据状态码去识别这个错误,因为状态码大概率返回的是200。
因此,要想让浏览器能够接收返回的数据,我们就要配置Access-Control-Allow-Origin
。
Access-Control-Allow-Origin
Access-Control-Allow-Origin
参数指定了单一的源,告诉浏览器允许该源访问资源。或者,对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符“*
”,表示允许来自任意源的请求。
其语法如下:
Access-Control-Allow-Origin: <origin> | *
比如,结合我们之前截图的示例,为了让浏览器允许来自https://www.baidu.com
的代码访问资源,我们可以这样配置此参数:
Access-Control-Allow-Origin: https://www.baidu.com
Vary: Origin
为什么这里还要配置一下Vary
参数?
如果服务端指定了具体的单个源(作为允许列表的一部分,可能会根据请求的来源而动态改变)而非通配符“
*
”,那么响应标头中的Vary
字段的值必须包含Origin
。这将告诉客户端:服务器对不同的Origin
返回不同的内容。——MDN
这段回答还是有几个名词需要解释一下才好理解:
至此,我们就完成了对第一个问题的回答:
跨源请求是发出浏览器了还是没有 ✔
接下来,我们继续聊一下刚刚提到的设置通配符时的前提条件:
对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符“
*
”
什么是身份凭证请求?如果我期望这个跨源请求能够携带身份凭证,我又该怎么做?
- 身份凭证请求:通常指的是那些用于用户身份验证的信息,其中最常见的例子就是
cookie
。
那么我该如何让一个跨源请求能够携带cookie
呢?
跨源请求携带cookie
这需要服务端和客户端配合,才能实现。
Access-Control-Allow-Credentials
在服务端,我们需要设置这个参数的值,布尔值。
Access-Control-Allow-Credentials
头指定了当浏览器的credentials
设置为 true 时是否允许浏览器读取 response 的内容。
将这个参数设置为true
后,则代表服务端明确允许浏览器发起的请求可以携带cookie
。
withCredentials
在客户端(也就是浏览器),通常情况下对于跨源请求,浏览器是不会携带身份凭证信息的(也就是cookie
),但是不会不代表不能。
备注: 当发出跨源请求时,第三方 cookie 策略仍将适用。无论如何改变本章节中描述的服务器和客户端的设置,该策略都会强制执行。——MDN
那么,客户端要在请求中携带cookie
,该如何配置呢?
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
设置withCredentials
的值为true
即可。
然后,我们再来聊一聊,前一阵子热度比较高的面试题:
为什么POST请求会发送两次?
为什么POST请求会发送两次
实际上,这个问题本身也是面试官埋的一个陷阱。如果你钻入POST这个陷阱,期望从POST请求本身探寻原因,那么就南辕北辙了。
重点不是POST,重点另有其人。
首先我们来聊一聊,为什么是两次请求,第一次请求和第二次的请求的区别是什么?
- 第一次请求:预检请求
- 第二次请求:真实请求
当我们听到这个问题的时候,至少我自己的脑海里,想的是,欸,好像我做业务时写的那些POST请求从来都没有发送两次过。
也就是说,这个两次其实并不是任何条件下都会发生的。
顺着这个逻辑,我们就可以推断出,第一次请求是有可能不会触发的,即:
预检请求的发送是存在条件限制的
ok,那么问题就转变成了:
什么条件下,才会导致浏览器会首先发送1次预检请求,从而导致请求总次数变成2次呢?
- 这个请求是跨源的
- 这个请求是非简单请求
什么是跨源请求咱们已经聊过了,那么接下来就聊一聊什么是非简单请求
简单请求
某些请求不会触发 CORS 预检请求。在废弃的 CORS 规范中称这样的请求为简单请求,但是目前的 Fetch 规范(CORS 的现行定义规范)中不再使用这个词语。
其动机是,HTML 4.0 中的
<form>
元素(早于跨站XMLHttpRequest
和fetch
)可以向任何来源提交简单请求,所以任何编写服务器的人一定已经在保护跨站请求伪造攻击(CSRF)。在这个假设下,服务器不必选择加入(通过响应预检请求)来接收任何看起来像表单提交的请求,因为 CSRF 的威胁并不比表单提交的威胁差。然而,服务器仍然必须提供Access-Control-Allow-Origin
的选择,以便与脚本共享响应。——MDN
上面这段话,前半段是很好理解的,后半段也许是中文翻译的问题,读来读去都感觉没读懂,不知道在讲什么。
我来再给大家做个中译中:
这段话主要讲的是
CORS
设计中的一个关键考虑:历史兼容性,即考虑到HTML 4.0中<form>
元素的行为。那么
<form>
元素到底有啥行为嘞?在HTML 4.0时代,表单(
<form>
)是网页上发送数据到服务器的主要方式。表单可以设置action
属性指向任何URL,这意味着表单可以提交到任何来源的服务器。由于
<form>
元素的这种行为,服务器开发者们必须采取措施防止跨站请求伪造攻击(CSRF)。也就是说,对于
CORS
跨域请求来说,服务器已经因为以前的<form>
元素行为,而存在保护机制,能够避免服务器遭受CSRF,因此,既然服务器能够处理来自表单的请求,那么它也能够处理来自脚本的同类型请求。所以,
CORS
允许简单请求直接发送给服务器,而无需预检请求。
不知道我的中译中你是否还满意?
如果你有更好的理解,欢迎评论区留言。
接下来我们聊以聊什么是简单请求
若请求满足所有下述条件,则该请求可视为简单请求:
-
使用下列方法之一:
-
除了被用户代理自动设置的标头字段(例如
Connection
、User-Agent
或其他在 Fetch 规范中定义为禁用标头名称的标头),允许人为设置的字段为 Fetch 规范定义的对 CORS 安全的标头字段集合。该集合为:Accept
Accept-Language
Content-Language
Content-Type
(需要注意额外的限制)Range
(只允许简单的范围标头值 如bytes=256-
或bytes=127-255
)
-
Content-Type
标头所指定的媒体类型的值仅限于下列三者之一:text/plain
multipart/form-data
application/x-www-form-urlencoded
-
如果请求是使用
XMLHttpRequest
对象发出的,在返回的XMLHttpRequest.upload
对象属性上没有注册任何事件监听器;也就是说,给定一个XMLHttpRequest
实例xhr
,没有调用xhr.upload.addEventListener()
,以监听该上传请求。 -
请求中没有使用
ReadableStream
对象。
举一个简单请求的代码例子:
GET /resources/public-data/ HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive Referer: https://wwwreferer.com/
Cookie: name=value; another_name=another_value
HTTP请求类型总览
- GET:请求指定的资源。使用GET方法的请求应该只用于获取数据,并且不应该产生副作用。
- POST:向服务器提交数据,通常用于创建新的资源或执行可能导致副作用(如数据库更新)的操作。
- PUT:用于更新资源。与POST不同,PUT通常用于更新现有资源,而且它是幂等的,即多次执行同一请求应该产生相同的效果。
- DELETE:请求删除指定的资源。
- HEAD:类似于GET请求,但服务器在响应中只返回首部,不返回主体内容。
- OPTIONS:用于描述目标资源的通信选项,通常用于跨源资源共享(CORS)的预检请求。
- PATCH:用于对资源进行部分更新。
- TRACE:回显服务器收到的请求,主要用于诊断。
- CONNECT:用于建立一个到由目标资源标识的服务器的隧道
非简单请求
举一个非简单请求的例子:
PUT /resources/private-data/12345 HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Content-Type: application/json
X-Custom-Header: CustomValue
Authorization: Bearer your_token_here
Connection: keep-alive
Referer: https://wwwreferer.com/
Cookie: name=value; another_name=another_value
Content-Length: 27
{"key1":"value1","key2":"value2"}
预检请求
一起来看一个预检请求的代码示例:
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.example
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
这里我们注意2个HTTP请求响应头:Access-Control-Request-Method
和Access-Control-Request-Headers
Access-Control-Request-Method
:在预检请求中发送的Access-Control-Request-Method
标头告知服务器实际请求所使用的 HTTP 方法,在这里将实际使用POST
请求方法。Access-Control-Request-Headers
:标头告知服务器实际请求所携带的自定义标头,在这里会使用X-PINGOTHER
和Content-Type
标头。-
X-PINGOTHER
-
定义:
X-PINGOTHER
是一个非标准的HTTP请求头,通常用于自定义的HTTP请求中,以携带特定的信息或执行特定的操作。由于它不是HTTP标准的一部分,因此它不被所有的服务器或客户端所识别或支持。 -
用途:
X-PINGOTHER
可能被用于多种场景,比如:- 开发者可以使用它来进行自定义的HTTP请求,比如ping操作,以检查服务器的可达性或特定功能的状态。
- 在某些跨源请求的例子中,
X-PINGOTHER
可能被用作自定义请求头来触发CORS(跨源资源共享)的预检请求。
-
注意事项:由于
X-PINGOTHER
不是标准头部,使用它时需要确保服务器端能够理解并正确处理这个头部。如果服务器没有配置为处理这个头部,它可能会被忽略或者导致请求失败。
-
-
Content-Type
-
定义:
Content-Type
是一个标准的HTTP请求和响应头部,用于指示资源的MIME类型(例如,text/html
、application/json
等)。 -
用途:
Content-Type
的用途包括:- 在请求中,它告诉服务器发送的数据类型,这样服务器可以知道如何处理和解析数据。
- 在响应中,它告诉客户端返回的数据类型,这样客户端可以正确地解析和处理数据。
-
示例:
Content-Type: text/html; charset=UTF-8
表示响应的内容是HTML文本,并且使用UTF-8字符编码。Content-Type: application/json
表示响应的内容是JSON格式的数据。
-
注意事项:
Content-Type
头部对于正确处理HTTP请求和响应至关重要。如果头部设置不正确,可能会导致服务器或客户端错误地解析数据。
-
-
大量的预检请求严重影响网络通信的速度,如何避免?
根据上文的内容,我们已经知道了预检请求发生的条件。但是假如我们系统存在大量的请求,这些请求都是非简单请求,它们都会发送预检请求,从而导致实际请求数为2次,这时我们该怎么呢?
在性能优化的实现里,其实就有一条减少网络请求的优化手段。因此,大量的预检请求一定会对我们的网页性能产生负面的影响,这不是我们期望看到的。
我们可以通过设置Access-Control-Max-Age
来避免上述情况。
Access-Control-Max-Age
Access-Control-Max-Age
头指定了预检请求的结果能够被缓存多久 ——MDN
它的使用如下:
Access-Control-Max-Age: <delta-seconds>
你可以这样设置一个有效期为15天的预检请求,这样在有效期内,它不会再次发送了。
Access-Control-Max-Age: 1296000
HTTP头
在上面章节的内容里,我们提到了很多HTTP头,其他的HTTP头我列在了下方,大家可以访问MDN文档进行查看,这里就不再赘述。
HTTP响应标头
跨源资源共享(CORS) - HTTP - HTTP响应标头 | MDN (mozilla.org)
HTTP请求标头
跨源资源共享(CORS) - HTTP - HTTP请求标头 | MDN (mozilla.org)
结语
好家伙,光是一个CORS
就能说这么多东西,不过我相信,如果你阅读完本文,应该足够应对有关CORS
的面试题了。
谁又说“完结篇”是1篇文章了呢,对吧?
期待与你在下一篇WebSocket
相遇~
转载自:https://juejin.cn/post/7407995254076407835