CORS详解

时间:2022-12-24 16:08:47
介绍

由于同源策略的缘故,以往我们跨域请求,会使用诸如JSON-P(不安全)或者代理(设置代理和维护繁琐)的方式。而跨源资源共享(Cross-Origin Resource Sharing)是一个W3C规范,其建立在XMLHttpRequest对象之上,允许开发人员像使用同源请求一样的规则,在浏览器端发送跨域请求。

CORS的使用场景很简单。例如,站点bob.com想要请求获取alice.com的数据,由于同源策略缘故,这种情况在传统请求中是不被允许的。然而,bob.com通过CORS请求alice.com,并在alice.com响应头中添加少许特殊的响应头,就可以达到bob.com获取到alice.com数据的目的。

正如你上面看到的例子,要实现CORS,需要客户端和服务端的共同协调。幸运的是,如果你是客户端开发人员,很多具体细节对于你来说是屏蔽的。好了,接下来我们将介绍客户端怎样发起跨域请求,以及服务端如何设置,从而达到支持CORS的目的。

发起一个CORS请求

该小节讲解了如何使用JavaScript发起一个跨域请求。

-创建XMLHttpRequest对象-

浏览器支持CORS情况,如下:

.Chrome 3+

.Firefox 3.5+

.Opera 12+

.Safari 4+

.Internet Explorer 8+

浏览器支持CORS情况,更多见http://caniuse.com/#search=cors

Chrome,Firefox,Opera 和 Safari都是使用XMLHttpRequest2对象。Internet Explorer使用了类似的对象XDomainRequest,其工作原理和XMLHttpRequest大致相同,但增加了额外的安全防范措施。

由于浏览器的差异,首先,你需要根据浏览器的不同,创建一个合适的请求对象。Nicholas Zakas写了一个简单的辅助方法,来屏蔽掉浏览器的差异,如下:

CORS详解
function createCORSRequest(method, url){
var xhr = new XMLHttpRequest();
if("withCredentials" in xhr){
//检查XHLHttpRequest对象是否有"withCredentials"属性
//"withCredentials"属性仅存在于XMLHttpReqeust2对象中
xhr.open(method, url, true);
}else if(typeof XDomainRequest !="undefined"){
//否则,检查XDomainRequest
//XDomainRequest仅存在IE中,且通过其发起CORS请求
xhr = new XDomainRequest();
xhr.open(method, url);
}else{
//否则,CORS不被该浏览器支持
xhr = null;
}
return xhr;
}
var xhr = createCORSRequest('GET', url);
if(!xhr){
throw new Error('CORS not supported');
}
CORS详解

-事件处理-

最初的XMLHttpRequest对象只有一个事件句柄:

onreadystatechange,处理所有的响应。虽然onreadystatechange仍然可用,但是XMLHttpRequest2引入了更多新的事件句柄,如下:

事件句柄

描述

onloadstart*

当请求发起时

onprogress

当加载和发送数据时

onabort*

当请求被中断时。例如,调用abort()方法

onerror

当请求失败时

onload

当请求成功时

ontimeout

当请求时间超过开发者设定时间时

onloadend*

当请求完成时(成功或失败)

上述,凡是带有星号(*)的事件句柄,IE的XDomainRequest都不支持。

来源:http://www.w3.org/TR/XMLHttpRequest2/#events

在大多数情况下,我们至少会使用onload和onerror事件:

CORS详解
xhr.onload = function(){
var responseText = xhr.responseText;
console.log(responseText);
//处理响应
};
xhr.onerror = function(){
console.log('There was an error!');
}
CORS详解

当请求出现错误时,浏览器并不能很友好地报告出具体的错误。比如,Firefox对所有的错误都会报告0状态和空状态文本。浏览器也能通过日志反馈错误信息,但是信息却不能被JavaScript获取。当处理onerror事件句柄时,你会知道有错误出现,除此之外,一无所获。

-withCredentials-

标准的CORS请求,默认情况下是不会发送或者设置cookie值的。为了在请求时,附带cookies,我们需要设置XMLHttpRequest的withCredentials属性为true:

xhr.withCredentials = true;

为了让其运作,服务端也必须在响应头中设置Access-Control-Allow-Credentials为true,开启credentials。如下:

Access-Control-Allow-Credentials: true;

设置withCredentials属性后,远程域请求时会带上所有cookies,以及设置它们。注意,这些cookie值仍然遵守同源策略,所以我们的JavaScript代码仍然不能从document.cookie或者响应头中获取cookie,它们仅仅被远程域控制。

-发送请求-

现在我们的CORS请求设置完毕,我们通过调用send()方法,即可发起该请求,如下:

xhr.send();

如果该请求有请求体,那么作为send方法中的参数,发送即可。

客户端的CORS就这样啦!假设服务端已经设置好了CORS,当服务端返回响应后,我们的onload事件句柄就会被触发,就像你熟悉的标准同源XHR请求一样。

-端到端例子-

下面就是一个完整的CORS示例。运行示例并在浏览器调试器中查看实际请求操作。

CORS详解
// 创建XHR 对象.
function createCORSRequest(method, url) {
var xhr = new XMLHttpRequest();
if ("withCredentials" in xhr) {
// XHR for Chrome/Firefox/Opera/Safari.
xhr.open(method, url, true);
} else if (typeof XDomainRequest != "undefined") {
// XDomainRequest for IE.
xhr = new XDomainRequest();
xhr.open(method, url);
} else {
// 不支持CORS.
xhr = null;
}
return xhr;
} // 辅助函数:解析响应内容中的title标签
function getTitle(text) {
return text.match('<title>(.*)?</title>')[1];
} // 发起CORS请求.
function makeCorsRequest() {
// HTML5 Rocks支持 CORS.
var url = 'http://updates.html5rocks.com'; var xhr = createCORSRequest('GET', url);
if (!xhr) {
alert('CORS not supported');
return;
} // 响应处理.
xhr.onload = function() {
var text = xhr.responseText;
var title = getTitle(text);
alert('Response from CORS request to ' + url + ': ' + title);
}; xhr.onerror = function() {
alert('Woops, there was an error making the request.');
}; xhr.send();
}
CORS详解
服务端配置CORS

CORS最繁重的处理是在浏览器和服务器之间。当浏览器发送一个CORS请求时,会添加一些额外的响应头,有时还会发送额外的请求。这些额外的步骤对于客户端人员来说,是透明的(但是我们可以通过一个包分析器去发现,例如Wireshark)。

CORS详解

浏览器制造商负责浏览器端的实现。该小节将阐述,服务端如何设置它的头部,从而达到支持CORS的目的。

-CORS请求类型-

跨域请求有两种形式:

1、  简单请求

2、  非简单请求

简单请求满足以下条件:

.HTTP请求方法(区分大小写)为以下之一:

。HEAD

。GET

。POST

.HTTP头部匹配(不区分大写小)为以下:

。Accept

。Accept-Language

。Content-Language

。Last-Event-ID

。Content-Type,但是赋值仅为以下之一:

-application/x-www-form-urlencoded

-multipart/form-data

-text/plain

简单请求的特征如上所诉,因为它们不需要使用CORS就可以在浏览器中发起跨域请求了。例如,JSON-P发起GET请求跨域,又如HTML利用POST提交表单。

其他任何请求,只要不满足以上条件的,都是非简单请求,且发起非简单请求时,在浏览器和服务器之间需要额外的通信(又叫预请求)。好了,下面我们就一同进入跨域之旅吧。

-处理一个简单请求-

我们从客户端发起一个简单请求开始。下面的代码展示了如何利用JavaScript发起一个简单请求GET,以及浏览器实际发出的HTTP请求。

JavaScript:

var url = 'http://api.alice.com/cors';
var xhr = createCORSRequest('GET', url);
xhr.send();

HTTP请求:

CORS详解
GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
CORS详解

值得注意的是,一个有效的CORS请求,总是包含一个Origin头部,而这个Origin头部又是浏览器自动添加的,用户操作不了。且,这个Origin头部的值是由协议(例如http),域名(例如bob.com)和端口(仅当不是默认端口时,包含,例如81)组成,如http://api.alice.com。

但也要注意,如果一个请求包含Origin头部,未必就是一个跨域请求。虽然所有的CORS请求都会包含一个Origin头部,但是一些同源请求可能也会包含它。例如,Firefox在发起同源请求时,不会包含一个Origin头部,但是Chrome和Safari下,除发起同源GET请求不会包含Origin头部外,发起同源POST/PUT/DELETE请求时,都会包含Origin头部。例如,下面就是一个包含Origin头部的同源请求:

POST /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.bob.com

好消息是,对于同源请求,浏览器不会期望服务器返回CORS响应头。因此不管是否有CORS标头,同源请求的响应都是直接发送给用户。然而,如果我们服务器代码返回一个错误,假设源信息Origin不在服务器请求列表中,那么要在头部Origin中包含请求源。

下面是一个关于CORS有效的服务器响应:

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true;
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

所有和CORS相关的头部都是以"Access-Control-"开头。更多,见下:

Access-Control-Allow-Origin(必须)-该请求头必须包含在所有合法的CORS响应头中;否则,省略该响应头会导致CORS请求失败。该值要么与请求头Origin的值一样(如上述例子),要么设置成星号‘*’,以匹配任意Origin。如果你想任何站点都能获取到你的数据,那么就使用‘*’吧。但是,如果你想有效的控制,就将该值设置为一个实际的值。

Access-Control-Allow-Credentials(可选)-默认情况下,发送CORS请求,cookies是不会附带发送的。但是,通过使用该响应头就可以让cookies包含在CORS请求中。注意,该响应头只有唯一的合法值true(全部小写)。如果你不需要cookies值,就不要包含该响应头了,而不是将该响应头的值设置成false。该响应头Access-Control-Allow-Credentials需要与XMLHttpRequest2对象的withCredentials属性配合使用。当这两个属性同时设置为true时,cookies才能附带。例如,withCredentials被设置成true,但是响应头中不包含 Access-Control-Allow-Credentials响应头,那么该请求就会失败(反之亦然)。发送CORS请求时,最好不要携带cookies,除非你确定你想在请求中包含cookie。

Access-Control-Expose-Headers(可选)-XMLHttpRequest2对象有一个getResponseHeader()方法,该方法返回一个特殊响应头值。在一个CORS请求中,getResponseHeader()方法仅能获取到简单的响应头,如下:

.Cache-Control

.Content-Language

.Content-Type

.Expires

.Last-Modified

.Pragma

如果你想客服端能够获取到其他的头部信息,你必须设置Access-Control-Expose-Headers响应头。该响应头的值可以为响应头的名称,不过需要利用逗号隔开,这样客服端就能通过getResponseHeader方法获取到了。

-处理一个非简单请求-

在上面,我们一起学习了简单请求GET,但是倘若我们想做更多的事情呢?比如,我们想使用PUT或者DELETE请求,又或者我们想使用Content-Type:application/json来支持JSON。那么,我们就需要掌握该节讲述的‘非简单请求’了。

我们在使用非简单请求时,表面上看起来客户端只发送了一个请求,但实际上,要完成一次非简单请求,客户端在私底下是要向服务器发起两次请求的。第一次请求,是向服务器确认权限,一旦被授权,则发起第二次请求(真正意义上的数据请求)。且,第一次请求也可以被缓存,所以不是每次我们发起非简单请求,都会预请求一次。

例,非简单请求如下:

JavaScript:

var url = 'http://api.alice.com/cors';
var xhr = createCORSRequest('PUT', url);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

上面代码中,HTTP请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header。

浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的HTTP头信息。

CORS详解
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
CORS详解

和简单请求一样,浏览器自动将Origin头部信息添加到每个请求中,包括这里的预检查请求。预检查请求用的方法是OPTIONS(所以请确保我们的服务器能够响应该方法)。且,它也包含两个特殊的头部信息,如下:

Access-Control-Request-Method:该字段表示实际的CORS是什么HTTP方法,如上述的PUT方法,且该字段是必须的,即使是简单请求的方法(GET,POST,HEAD)。

Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,如上述的X-Custom-Header。

在上面我们已经提到,预检查请求的目的是向服务器确认实际的 CORS请求权限,那么它是如何检查的呢。

其实,就是验证预检查请求中的两个特殊的请求头(Access-Control-Request-Method和Access-Control-Request-Headers)来裁定的。服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就做如下响应:

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8

Access-Control-Allow-Origin(必须)—和简单请求一样,预检查响应也必须包含该头部,具体描述详见简单请求中的Access-Control-Allow-Origin。

Access-Control-Allow-Methods (必须)--它是逗号分隔的一个字符串,值由HTTP方法构成,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。因为已提过预检查请求可以被缓存,所以这样可以避免多次"预检"请求。

Access-Control-Allow-Headers--如果浏览器请求包括Access-Control-Request-Headers字段,则该字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段,因为可以缓存嘛。

Access-Control-Allow-Credentials(可选)—和简单请求一样,详见上述简单请求中的该字段。

Access-Control-Max-Age(可选)--如果每次发起一个非简单的CORS请求,都暗地向服务器发送两次请求,那代价也太大了点,所以该字段可以指定预检查请求可以被缓存多少秒。

一旦预检查得到授权信息,那么浏览器就会发送真正的跨域请求了。且,请求和服务器响应与简单CORS请求一样。

第二次请求(实际CORS请求),如下:

CORS详解
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
CORS详解

响应如下:

Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

如果服务端想要拒绝该CORS请求,那么它可以返回一个普通的响应(如HTTP 200),即不包含任何属于CORS的头部信息。如果预检查请求没有被审核通过,即没有任何关于CORS头部信息的响应,那么浏览器是不会发起第二次实际的请求的,如下服务器响应预检查请求:

//错误-没有CORS头部信息,所以表示是一个无效请求
Content-Type: text/html; charset=utf-8

且会触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息:

CORS详解