同源策略
处于安全的因素,浏览器限制了从脚本发起跨域的 HTTP 请求。XMLHttpRequest
和其他 Fetch 接口
会遵循 同源策略(same-origin policy)
。也就是说使用这些 API 的应用程序想要请求相同的资源,那么他们应该具有相同的来源,除非来自其他来源的响应包括正确的 CORS 标头也可以。
同源策略是一种很重要的安全策略,它限制了从一个来源加载的文档或脚本如何与另一个来源的资源进行交互。它有助于隔离潜在的恶意文档,减少可能的攻击媒介。
我们上面提到,如果两个 URL 具有相同的协议、主机和端口号(如果指定)的话,那么两个 URL 具有相同的来源。下面有一些实例,你判断一下是不是具有相同的来源
目标来源 http://store.company.com/dir/page.html
现在我带你认识了两遍不同的源,现在你应该知道如何区分两个 URL 是否属于同一来源了吧!
好,你现在知道了什么是跨域问题,现在我要问你,哪些请求会产生跨域请求呢?这是我们下面要讨论的问题
跨域请求
跨域请求可能会从下面这几种请求中发出:
- 调用
XMLHttpRequest
或者Fetch
api。
XMLHttpRequest 是什么?(我是后端程序员,前端不太懂,简单解释下,如果解释的不好,还请前端大佬们不要胖揍我)
所有的现代浏览器都有一个内置的 XMLHttpReqeust
对象,这个对象可以用于从服务器请求数据。
XMLHttpReqeust 对于开发人员来说很重要,XMLHttpReqeust 对象可以用来做下面这些事情
- 更新网页无需重新刷新页面
- 页面加载后从服务器请求数据
- 页面加载后从服务端获取数据
- 在后台将数据发送到服务器
使用 XMLHttpRequest(XHR) 对象与服务器进行交互,你可以从 URL 检索数据从而不必刷新整个页面,这使网页可以更新页面的一部分,而不会中断用户的操作。XMLHttpRequest 在 AJAX
异步编程中使用很广泛。
再来说一下 Fetch API 是什么,Fetch 提供了请求和响应对象(以及其他网络请求)的通用定义。它还提供了相关概念的定义,例如 CORS 和 HTTP Origin 头语义,并在其他地方取代了它们各自的定义。
- Web 字体(用于 CSS 中@ font-face中的跨域字体使用),以便服务器可以部署 TrueType 字体,这些字体只能由允许跨站点加载和使用的网站使用。
- WebGL 纹理
- 使用
drawImage()
绘制到画布上的图像/视频帧 - 图片的 CSS 形状
跨域功能概述
跨域资源共享标准通过添加新的 HTTP 标头来工作,这些标头允许服务器描述允许哪些来源从 Web 浏览器读取信息。另外,对于可能导致服务器数据产生副作用的 HTTP 请求方法(尤其是 GET 或者具有某些 MIME 类型 POST 方法以外 HTTP 方法),该规范要求浏览器预检
请求,使用 HTTP OPTIONS 请求方法从服务器请求受支持的方法,然后在服务器批准
后发送实际请求。服务器还可以通知客户端是否应与请求一起发送凭据
(例如 Cookies 和 HTTP 身份验证)。
注意:CORS 故障会导致错误,但是出于安全原因,该错误的详细信息不适用于 JavaScript。所有代码都知道发生了错误。确定具体出问题的唯一方法是查看浏览器的控制台以获取详细信息。
访问控制
下面我会和大家探讨三种方案,这些方案都演示了跨域资源共享的工作方式。所有这些示例都使用XMLHttpRequest,它可以在任何支持的浏览器中发出跨站点请求。
简单请求
一些请求不会触发 CORS预检
(关于预检我们后面再介绍)。简单请求
是满足一下所有条件的请求
- 允许以下的方法:
GET
、HEAD
和POST
- 除了由用户代理自动设置的标头(例如 Connection、User-Agent 或者在 Fetch 规范中定义为禁止标头名称的其他标头)外,唯一允许手动设置的标头是那些 Fetch 规范将其定义为
CORS安全列出的请求标头
,它们是:
-
- Accept
- Accept-Language
- Content-Language
- Content-Type(下面会介绍)
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
- Content-Type 标头的唯一允许的值是
-
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- 没有在请求中使用的任何 XMLHttpRequestUpload 对象上注册事件侦听器;这些可以使用XMLHttpRequest.upload 属性进行访问。
- 请求中未使用 ReadableStream对象。
例如,假定 web 内容https://foo.example
想要获取https://bar.other
域的资源,那么 JavaScript 中的代码可能会像下面这样写
const xhr = new XMLHttpRequest(); const url = 'https://bar.other/resources/public-data/'; xhr.open('GET', url); xhr.onreadystatechange = someHandler; xhr.send();
这使用 CORS 标头来处理特权,从而在客户端和服务器之间执行某种转换。
让我们看看在这种情况下浏览器将发送到服务器的内容,并让我们看看服务器如何响应:
GET /resources/public-data/ HTTP/1.1
Host: bar.otherUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
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
注意请求的标头 Origin ,它表明调用来自于 https://foo.example
。让我们看看服务器是如何响应的
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
[…XML Data…]
服务端发送 Access-Control-Allow-Origin
作为响应。使用 Origin
标头和 Access-Control-Allow-Origin
展示了最简单的访问控制协议。在这个事例中,服务端使用 Access-Control-Allow-Origin
作为响应,也就说明该资源可以被任何域访问。
如果位于https://bar.other
的资源所有者希望将对资源的访问限制为仅来自https://foo.example
的请求,他们应该发送如下响应
Access-Control-Allow-Origin: https://foo.example
现在除了 https://foo.example
之外的任何域都无法以跨域方式访问到 https://bar.other
的资源。
预检请求
和上面探讨的简单请求不同,预检
请求首先通过 OPTIONS
方法向另一个域上的资源发送 HTTP 请求,用来确定实际请求是否可以安全的发送。跨站点这样被预检
,因为它们可能会影响用户数据。
下面是一个预检事例
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://bar.other/resources/post-here/';);
xhr.setRequestHeader('X-PINGOTHER', 'pingpong');
xhr.setRequestHeader('Content-Type', 'application/xml');
xhr.onreadystatechange = handler;
xhr.send('<person><name>Arun</name></person>');
上面的事例创建了一个 XML 请求体用来和 POST 请求一起发送。此外,设置了非标准请求头 X-PINGOTHER
,这个标头不是 HTTP/1.1 的一部分,但通常对 Web 程序很有用。由于请求的 Content-Type
使用 application/xml
,并且设置了自定义标头,因此该请求被预检
。如下图所示
如下所述,实际的 POST 请求不包含 Access-Control-Request- * 标头;只有 OPTIONS 请求才需要它们。
下面我们来看一下完整的客户端/服务器交互,首先是预检请求/响应
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
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: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
上面的1 -11 行代表预检请求,预检请求使用 OPYIIONS
方法,浏览器根据上面的 JavaScript 代码段所使用的请求参数确定是否需要发送此请求,以便服务器可以响应是否可以使用实际请求参数发送请求。OPTIONS 是一种 HTTP / 1.1方法,用于确定来自服务器的更多信息,并且是一种安全的方法,这意味着它不能用于更改资源。请注意,与 OPTIONS 请求一起,还发送了另外两个请求标头(分别是第9行和第10行)
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
Access-Control-Request-Method
标头作为预检请求的一部分通知服务器,当发送实际请求时,将使用POST
请求方法发送该请求。
Access-Control-Request-Headers
标头通知服务器,当发送请求时,它将与X-PINGOTHER 和 Content-Type 自定义标头一起发送。服务器可以确定这种情况下是否接受请求。
下面的 1 - 11行是服务器发回的响应,表示POST
请求和 X-PINGOTHER
是可以接受的,我们着重看一下下面这几行
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
服务器完成响应表明源 http://foo.example
是可以接受的 URL,能够允许 POST、GET、OPTIONS
进行请求,允许自定义标头 X-PINGOTHER, Content-Type
。最后,Access-Control-Max-Age
以秒为单位给出一个值,这个值表示对预检请求的响应可以缓存多长时间,在此期间内无需发送其他预检请求。
完成预检请求后,将发送实际请求:
POST /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
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
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: https://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: https://foo.example
Pragma: no-cache
Cache-Control: no-cache
<person><name>Arun</name></person>
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain
[Some GZIP'd payload]
正式响应中很多标头我们在之前的文章已经探讨过了,本篇不再做详细的介绍,读者可以参考你还在为 HTTP 的这些概念头疼吗? 查阅
带凭证的请求
XMLHttpRequest 或 Fetch 和 CORS 最有趣的功能就是能够发出知道 HTTP Cookie 和 HTTP 身份验证的 凭证
请求。默认情况下,在跨站点 XMLHttpRequest 或 Fetch 调用中,浏览器将不发送凭据。调用 XMLHttpRequest对象或 Request 构造函数时必须设置一个特定的标志。
在下面这个例子中,最初从 http://foo.example
加载的内容对设置了 Cookies 的 http://bar.other
上的资源进行了简单的 GET 请求, foo.example 上可能的代码如下
const invocation = new XMLHttpRequest();
const url = 'http://bar.other/resources/credentialed-content/';;
function callOtherDomain() {
if (invocation) {
invocation.open('GET', url, true);
invocation.withCredentials = true;
invocation.onreadystatechange = handler;
invocation.send();
}
}
第7行显示 XMLHttpRequest 上的标志,必须设置该标志才能使用 Cookie 进行调用。默认情况下,调用是不在使用 Cookie 的情况下进行的。由于这是一个简单的 GET 请求,因此不会进行预检,但是浏览器将拒绝任何没有 Access-Control-Allow-Credentials 的响应:标头为true,指的是响应不会返回 web 页面的内容。
上面的请求用下图可以表示
这是客户端和服务器之间的示例交换:
GET /resources/access-control-with-credentials/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
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
Referer: http://foo.example/examples/credential.html
Origin: http://foo.example
Cookie: pageAccess=2
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:34:52 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 106
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
[text/plain payload]
上面第10行包含指向http://bar.other
上的内容 Cookie,但是如果 bar.other 没有以 Access-Control-Allow-Credentials:true
响应(下面第五行),响应将被忽略,并且不能使用网站返回的内容。
请求凭证和通配符
当回应凭证请求时,服务器必须在 Access-Control-Allow-Credentials
中指定一个来源,而不能直接写 通配符
因为上面示例代码中的请求标头包含 Cookie 标头,如果 Access-Control-Allow-Credentials
中是指定的通配符 的话,请求会失败。
注意上面示例中的 Set-Cookie
响应标头还设置了另外一个值,如果发生故障,将引发异常(取决于所使用的API)。
</div>