介绍 |
由于同源策略的缘故,以往我们跨域请求,会使用诸如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写了一个简单的辅助方法,来屏蔽掉浏览器的差异,如下:
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‘); }
-事件处理-
最初的XMLHttpRequest对象只有一个事件句柄:
onreadystatechange,处理所有的响应。虽然onreadystatechange仍然可用,但是XMLHttpRequest2引入了更多新的事件句柄,如下:
事件句柄 |
描述 |
onloadstart* |
当请求发起时 |
onprogress |
当加载和发送数据时 |
onabort* |
当请求被中断时。例如,调用abort()方法 |
onerror |
当请求失败时 |
onload |
当请求成功时 |
ontimeout |
当请求时间超过开发者设定时间时 |
onloadend* |
当请求完成时(成功或失败) |
上述,凡是带有星号(*)的事件句柄,IE的XDomainRequest都不支持。
来源:http://www.w3.org/TR/XMLHttpRequest2/#events
在大多数情况下,我们至少会使用onload和onerror事件:
xhr.onload = function(){ var responseText = xhr.responseText; console.log(responseText); //处理响应 }; xhr.onerror = function(){ console.log(‘There was an error!‘); }
当请求出现错误时,浏览器并不能很友好地报告出具体的错误。比如,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示例。运行示例并在浏览器调试器中查看实际请求操作。
// 创建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请求时,会添加一些额外的响应头,有时还会发送额外的请求。这些额外的步骤对于客户端人员来说,是透明的(但是我们可以通过一个包分析器去发现,例如Wireshark)。
浏览器制造商负责浏览器端的实现。该小节将阐述,服务端如何设置它的头部,从而达到支持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请求:
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请求,总是包含一个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头信息。
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...
和简单请求一样,浏览器自动将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请求),如下:
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...
响应如下:
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回调函数捕获。控制台会打印出如下的报错信息:
原文:https://www.html5rocks.com/en/tutorials/cors/?redirect_from_locale=zh