CORS詳解


介紹

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM