背景
前端和后台请求交互,若请求者(Client)和资源者响应者(Server)处在不同的域名,会发生跨域请求,不同域是指HTTP协议、域名、端口有一个或全都不相同。出于安全考虑浏览器限制了从脚本内发起的资源跨域请求,CORS是解决跨域请求的一种机制。
一、什么CORS
CORS全称“跨域资源共享”,它是一种机制通过添加额外的HTTP头部告诉浏览器,允许origin上的web应用访问不同源服务器资源,可以在XMLHttpRequest、Fetch中使用CORS发起跨域请求。
二、CORS工作原理
对于前端开发使用CORS发起跨域请求不需要额外工作,当请求发生时浏览器会自动设置相应的HTTP头部信息(下面介绍道),服务器端需要做相应的头部设置来控制跨域权限。
1. 发起请求
对于使用CORS发起跨域请求可分为简单请求和非简请求两种,简单请求不会触发OPTIONS预检请求,可直接发起跨域请求,非简单请求会触发OPTIONS预检请求,向服务器发起一些询问信息,例如:是否允许这次跨域请求、告知用什么请求方法、是否支持在HTTP头里携带自定义字段。
1.1 简单请求
简单请求可直接向服务器请求,只要服务器同意这次跨域求,浏览器就能获取到服务器返回的资源,在请求中只要同时满足以下条件就属于简单请求。
a. 使用GET、POST、HEAD 三者之一的请求方法。
b. 不得设置Accept 、 Accept-Language、 Content-Language,Content-Type之外的头部信息集合字段,对于Content-Type的值不能设置text/plain, multipart/form-data, application/x-www-form-urlencode之外的 值。
c. 请求中的任意XMLHttpRequestupload对象均未设置任何监听器。
d. 请求中没有使用 ReadableStream 对象。
简单例子
var request = new XMLHttpRequest()
var url = 'http://bar.other/resources/public-data/'
function callOtherDomain() {
if(invocation) {
invocation.open('GET', url, true);
invocation.onreadystatechange = handler;
invocation.send();
}
}
//发起请求
callOtherDomain()
复制代码
请求发出时Client与Server间通过HTTP头部字段处理跨域权限。
HTTP请求头信息
1. GET /resources/public-data/ HTTP/1.1
2. Host: bar.other
3. User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US;
4. rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
5. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
6. Accept-Language: en-us,en;q=0.5
7. Accept-Encoding: gzip,deflate
8. Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
9. Connection: keep-alive
10. Referer: http://foo.example/examples/access-control/simpleXSInvocation.html
11. Origin: http://foo.example
复制代码
服务器响应头部信息
1. HTTP/1.1 200 OK
2. Date: Mon, 01 Dec 2008 00:23:53 GMT
3. Server: Apache/2.0.61
4. Access-Control-Allow-Origin: *
5. Keep-Alive: timeout=2, max=100
6. Connection: Keep-Alive
7. Transfer-Encoding: chunked
8. Content-Type: application/xml
复制代码
Client通过Origin字段告诉Server我是来自http://foo.example
的请求,允许我访问你的资源吗?Server做出回应并在响应头里带上Access-Control-Allow-Origin:* 表示我允许所有的外域访问我的资源。
如果将Access-Control-Allow-Origin值设置成http://foo.example
,表示Server上的资源只允许来自http://foo.example
的源访问。后台开发一般会在此添加请求源白名单来控制允许那些请求源访问服务资源。
如果Origin不是Access-Control-Allow-Origin允许的源,浏览器将拦截请求响应内容,无法获取到服务器资源。
1.2 非简单请求
只要不满足简单请求的都属于非简单请求。非简单请求会在正式发起资源访问请求前触发一个options 预检请求,options请求不会对服务器造成资源影响。 只要满足下列条件之一就会触发预检请求:
a. PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH。
b. 在请求头中设置Accept Accept-Language、 Content-Language、 Content-Type (需要注意额外的限制)、 DPR、 Downlink、 Save-Data、 Viewport-Width、 Width的字段。
c. Content-Type 的值不属于application/x-www-form-urlencoded、 multipart/form-data、 text/plain三者之一。
d. 请求中的XMLHttpRequestUpload 对象注册了任意多个事件监听器。
e. 请求中使用了ReadableStream对象。
再次借用MSDN中的例子说下
var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/post-here/';
var body = '<?xml version="1.0"?><person><name>Arun</name></person>';
function callOtherDomain(){
if(invocation)
{
invocation.open('POST', url, true);
invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
invocation.setRequestHeader('Content-Type', 'application/xml');
invocation.onreadystatechange = handler;
invocation.send(body);
}
}
// 发起请求
callOtherDomain()
复制代码
在代码中可以看到,我们人为的自定义了X-PINGOTHER
请求头字段(满足非简单请求b条件),并且Content-Type
被设置application/xml (满足非简单请求c条件),所以该请求首先会触发一个options请求。
下面是options请求头的部分信息:
1.OPTIONS /resources/post-here/ HTTP/1.1
2.Host: bar.other
3.User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
4.Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
5.Accept-Language: en-us,en;q=0.5
6.Accept-Encoding: gzip,deflate
7.Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
8.Connection: keep-alive
9.Origin: http://foo.example
10.Access-Control-Request-Method: POST
11.Access-Control-Request-Headers: X-PINGOTHER, Content-Type
复制代码
options 请求告诉Server, 我是来自http://foo.example
的请求,在正式请求时,我会在请求头中携带自定义字段X-PINGOTHER、Conten-Type并且我将通过POST发起请求,你同意不?以下是用于向Server询问的头部信息字段:
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
复制代码
我们看下Server对options请求是如何回应的
1.HTTP/1.1 200 OK
2.Date: Mon, 01 Dec 2008 01:15:39 GMT
2.Server: Apache/2.0.61 (Unix)
3.Access-Control-Allow-Origin: http://foo.example
4.Access-Control-Allow-Methods: POST, GET, OPTIONS
5.Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
6.Access-Control-Max-Age: 86400
7.Vary: Accept-Encoding, Origin
8.Content-Encoding: gzip
9.Content-Length: 0
10.Keep-Alive: timeout=2, max=100
11.Connection: Keep-Alive
12.Content-Type: text/plain
复制代码
从上面的options响应头里得知, 首部字段 Access-Control-Allow-Origin表明服务器允许http://foo.example请求源访问资源。 字段 Access-Control-Allow-Headers 表明服务器允许请求头中携带字段 X-PINGOTHER 与 Content-Type。 Access-Control-Allow-Methods告知允许正式请求使用POST方法。 options请求结束,如果Server同意访问,接下来就会发起正式的请求,否则结束请求。
第6行代码中有一段Access-Control-Max-Age: 86400
信息,这段信息的意思是说在接下来的86400秒内对于同一请求不用再发起options预检请求。Access-Control-Max-Age单位是秒,由后台设置但不能超过浏览器支持的最大有效时间,否则不会生效。
2. 附带身份凭证的请求
Fetch 与 CORS 可以基于 HTTP cookies 和 HTTP 认证信息发送身份凭证。一般而言,对于跨域 XMLHttpRequest 或 Fetch 请求,浏览器不会发送身份凭证信息。
如果要发送凭证信息,需要设置 XMLHttpRequest 的withCredentials=true,同时Server端也要设置响应的头部字段Access-Control-Allow-Credentials: true,才能在请求中携带身份凭证,否则,浏览器会拦截响应内容,不会把它返回给请求发送者。
需要注意,Server如果设置了Access-Control-Allow-Origin:*
同时请求又携带了cookies信息,会导致请求失败。
3. 跨域请求头和响应头字段说明
3.1 请求头字段
Origin字段表明预检请求或实际请求的源站,origin 参数的值为源站 URI。它不包含任何路径信息,只是服务器名称,不管是否为跨域请求都会发送Origin字段。
Origin: <origin>
复制代码
Access-Control-Request-Method字段用于预检请求。其作用是,将实际请求所使用的 HTTP方法告诉服务器。
Access-Control-Request-Method: <method>
复制代码
Access-Control-Request-Headers字段用于预检请求。其作用是,将实际请求所携带的首部字段告诉服务器。
Access-Control-Request-Headers: <field-name>[, <field-name>]*
复制代码
3.2 响应头字段
Access-Control-Allow-Origin字段,origin 参数的值指定了允许访问该资源的外域 URI。对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符,表示允许来自所有域的请求。
Access-Control-Allow-Origin: <origin> | *
复制代码
Access-Control-Expose-Headers字段,设置浏览器可以拿到的头部字段白名单。 在跨域访问时,XMLHttpRequest对象的getResponseHeader()方法只能拿到一些最基本的响应头,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果要访问其他头,需要服务器设置本响应头,这就是Access-Control-Expose-Headers的作用。
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
复制代码
Access-Control-Max-Age字段,设置预检测请求能缓存多久,单位秒。
Access-Control-Max-Age: <delta-seconds>
复制代码
Access-Control-Allow-Credentials字段,指定了当浏览器的credentials设置为true时是否允许浏览器读取response的内容
Access-Control-Allow-Credentials: true
复制代码
Access-Control-Allow-Methods字段用于预检请求的响应。其指明了实际请求所允许使用的 HTTP 方法。
Access-Control-Allow-Methods: <method>[, <method>]*
复制代码
Access-Control-Allow-Headers 首部字段用于预检请求的响应。其指明了实际请求中允许携带的首部字段。
Access-Control-Allow-Headers: <field-name>[, <field-name>]*
复制代码
4.兼容性
目前除了IE9及以下浏览不支持CORS外,其他浏览器基本的已经支持CORS。对于IE9及以下的浏览器可通过 XDomainRequest实现。
以上是我对CORS知识的梳理和记录,因为在以往工作中频繁遇到跨域问题,思来想去决定记录下来,为确保信息的正确性,极大的参照来官方文档,希望对有需要的小伙伴在处理跨域问题上有所帮助。
声明:本文的内容参考于MDN官方文档,包括上面用到的事例代码也是借用自MDN,若有侵权,请联系我删除相关内容。