前言
最近做的几个页面需要调用多个服务端的API,而在调用的时候时不时就会出现跨域的问题,等把问题解决之后才发现原来如此,所以想着能够记录一下以防后面再忘了
什么是CORS
CORS,全称 "跨域资源共享"(Cross-Origin resource share) 是一个W3C标准,它允许浏览器跨源向其他源的服务器请求资源;比如:一个网站上的 JavaScript 代码试图访问另一个网站的数据,或者在开发时 AJAX 接口请求另一个域名下的数据等
为什么会有CORS呢
之所以需要CORS,就是因为浏览器的同源策略( same origin policy)的限制,而同源策略是浏览器安全的基石,它能够保证用户信息的安全
什么是同源策略呢
同源策略是 1995年由Netscape公司引入浏览器的,到了现在基本所有浏览器都支持这种策略;而所谓的同源指的就是三个相同:
- 协议协同
- 域名相同
- 端口相同
比如,网址:http://www.baidu.com/dir/test.html,协议:htttp,域名:www.baidu.com,端口:80(默认的就可以被省略掉);其他网址与它的同源情况:
https://www.baidu.com/dir/test.html:不同源,因为协议不一样http://www.baidu.com:8080/dir/test.html:不同源,端口不一样http://www.baidu.com/test:同源
同源策略的目的就是保证用户信息的安全,防止被恶意用户窃取;比如:你在站点A登录,然后又去浏览其他的网站,如果其他的网站可以获取到A站点的Cookie,那是不是很可怕呢?
大部分时间Cookie中会存当前用户的登录状态,认证信息(比如:token);如果该用户没有登出,那么是不是就可以冒充该用户,这岂不是能够为所欲为了吗
同源策略主要的限制范围有如下三种
cookie,localStorage和indexDB无法读取DOM(Document Object Model)无法获得AJAX请求不能发送
虽然同源策略能够保证用户的信息安全,但有时候使用也确实不是很方便,一些正常的用途也会受到影响;下面就来说说如何规避同源限制
如何规避同源限制呢
规避同源限制的不同行为有多种方法,比如:跨域读取cookie,那么就可以将cookie写到一级域名下面,一级域名相同而二级域名不同的网页就能相互读取写入的cookie了
当然,也有方法能够规避其他同源限制行为,本文主要讨论的是规避AJAX请求的同源限制行为,规避其他限制行为可参考这篇文章
如何使用CORS规避AJAX的请求的同源限制
CORS是浏览器行为(服务和服务之间调用不存在),不需要用户参与,但是需要服务器端能够支持;现在的浏览器基本上都支持CORS功能
CORS的原理就是服务器在Response中添加一些HTTP Headers,告诉浏览器哪些源(origin)可以读取这些信息;因此,CORS的关键就是服务器,只要服务器支持了CORS,就可以跨源通信了
简单请求和非简单请求
在通信的时候,浏览器将CORS请求分为两类:简单请求(simple request)和非简单请求(not-so-simple-request)
只要同时满足以下条件的就是简单请求:
-
请求方法是以下三种方法之一:
- HEAD
- GET
- POST
-
除了用户代理(即:浏览器)自动设置的请求头以外,最多只能有如下的几种请求头:
- Accpet
- Accept-Language
- Content-Language
- Content-Type (仅限于三个值:
application/x-www-form-urlencoded,multipart/form-data,text/plain) - Range (仅限于简单的
range value,比如:bytes=256-或者bytes=127-200)
-
如果使用
XMLHttpRequest对象发送请求,没有在XMLHttpRequest.upload属性上注册事件监听器;比如:没有像这样xhr.upload.addEventListener()的代码 -
在请求中没有使用
ReadableStream
凡是不能同时满足如上条件的,就属于非简单请求;浏览器对简单请求和非简单请求的处理是不一样的
简单请求
下面通过一个例子来说明简单请求;假设https://foo.exmaple的网页想要调用获取https://bar.other服务的内容,发送的请求如下:
GET /resources/public-data/ 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: https://foo.example
发送的请求要是简单请求,浏览器会添加一个Origin header表示是由那个源发出的请求;这个源包含了协议,域名和端口
服务器受到请求之后根据这个Origin来判断是否允许此次请求;下面是服务器返回的Response header:
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: *表示该资源可以被任何源的请求访问;如果只允许被一个源访问,可以指定为:
Access-Control-Allow-Origin: https://foo.example
这样就表示只允许被当前这个请求源访问;如果当前请求的Origin不在指定的请求源范围之内,服务器也会正常返回;浏览器收到请求之后发现header中没有Access-Control-Allow-Origin,就知道请求不被允许,就会抛出一个错误,这种错误有可能不会被捕捉到,因为状态码是200
非简单请求
和简单请求不一样的是,非简单请求浏览器会首先发送一个options方法的请求,目的就是检查真实请求是否可以安全的发送,这种请求被称为"预检请求"(preflighted request)
浏览器使用预检请求主要就是询问服务器,请求的资源是否允许该网页访问,以及请求的方法和一些自定义的header是否被允许等;
只有服务器确定允许之后,才会发送真实的请求,否则请求就会直接报错了
下面这个例子就是需要浏览器发送预检请求的例子:
const xhr = new XMLHttpRequest();
xhr.open("POST", "https://bar.other/doc");
xhr.setRequestHeader("X-PINGOTHER", "pingpong");
xhr.setRequestHeader("Content-Type", "text/xml");
xhr.onreadystatechange = handler;
xhr.send("<person><name>Arun</name></person>");
请求是一个Post方法的请求,但是请求header中包含了自定义请求头X-PINGOTHER,并且Content-Type:text/xml也不在简单请求允许的范围内,所以首先就会发送一个预检请求:
OPTIONS /doc 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: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
预检请求的方法是options方法,Origin表明请求的来源;注意,在header中还有两个请求头:
Access-Control-Request-Method: POST:该字段作为预检请求的一部分,是用来通知服务器真实的请求会使用Post请求发送Access-Control-Request-Header:X-PINGOTHER, Content-Type:该字段是告诉服务器真实的请求会带有这些header
服务器收到预检请求之后就需要决定是否可以在这些条件下接受真实请求;如果接受,那么发送的响应如下所示:
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
在上面的请求头中:
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表示此次预检请求有效期,也就是说在多长时间以内发送不需要发送预检请求,直接发送真实请求就可以了,时间单位是秒
如果服务器否定了预检请求,就会返回一个正常的响应,但是不会有cors相关的header字段;这时,浏览器就会认为服务器不允许预检,因此会触发一个错误,捕获之后可以打印错误信息,如下
XMLHttpRequest cannot load https://foo.example.
Origin https://foo.example is not allowed by Access-Control-Allow-Origin.
如果预检请求通过之后,在有效期内就可以像发送简单请求那样发送这个post请求了;具体请求如下:
POST /doc 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>
Response如下:
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 XML payload]
有时候服务器可能会回应如下与cors相关的header:
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: X-PINGOTHER
Access-Control-Expose-Headers: X-PINGOTHER:可选,简单的CORS请求时,`XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('X-PINGOTHER')可以返回X-PINGOTHER字段的值。`Access-Control-Allow-Credentials: true:可选,这个值一般表示是否发送凭证相关的信息;下面具体来了解以下这个字段
Requests With Credentials
默认情况下,浏览器的cors请求是不会发送认证信息的,比如:cookie,basicAuth等;如果在XMLHttpRequest对象或者是Request的构造器中设置了指定的属性,那么就会发送这些cookie,basicAuth等信息到服务器了
下面是一个例子,请求源为https://foo.exmaple,它设置了cookie,向服务器http://bar.other发送一个简单请求,如下:
const invocation = new XMLHttpRequest();
const url = "https://bar.other/resources/credentialed-content/";
function callOtherDomain() {
if (invocation) {
invocation.open("GET", url, true);
invocation.withCredentials = true;
invocation.onreadystatechange = handler;
invocation.send();
}
}
其中,invocation.withCredentials = true;就表示请求会发送cookie到服务器,如果不设置,或者设置为false就不会发送cookie到服务器中了
虽然这是一个简单请求,但是server的响应中也必须要带有header Access-Control-Allow-Credentials:true,否则浏览器就会拒绝此次响应
下面是一个简单的例子:
GET /resources/credentialed-content/ 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: https://foo.example/examples/credential.html
Origin: https://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]
在cors预检请求(preflight)中绝不能包含认证信息,而对于预检请求的响应中必须指定Access-Control-Allow-Credentials: true表明真实请求需要发送cookie等认证信息
对于带有认证请求的响应中,有如下的限制:
Access-Control-Allow-Origin的值不能为*,必须指定一个确切的值,比如:Access-Control-Allow-Origin: https://example.comAccess-Control-Allow-Headers的值不能指定为*,必须指定一个列表,比如:Access-Control-Allow-Headers: X-PINGOTHER, Content-TypeAccess-Control-Allow-Methods的值不能为*,必须指定一个允许的列表,比如:Access-Control-Allow-Methods: POST, GETAccess-Control-Expose-Headers的值不能指定为*,必须指定一个列表,比如:Access-Control-Expose-Headers: Content-Encoding, Kuma-Revision
如果请求中包含一个cookie,响应为Access-Control-Allow-Origin: *,浏览器就会阻断访问,报告一个cors相关的错误
同样的,如果Access-Control-Allow-Origin: *,此时去设置cookie,那么这个cookie也不会被设置成功
第三方cookie
在cors响应中设置cookie也会受到第三方cookie策略的限制;比如,上面的例子中,bar.other尝试在foo.exmple下面设置cookie,如果浏览器配置了拒绝第三方cookie的策略,那么也会cookie不能成功设置;正常情况下,都是会阻止第三方cookie设置的
参考连接
developer.mozilla.org/en-US/docs/…
developer.mozilla.org/en-US/docs/…