介紹 |
由於同源策略的緣故,以往我們跨域請求,會使用諸如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回調函數捕獲。控制台會打印出如下的報錯信息: