1. 程式人生 > >跨域資源共享(CORS)

跨域資源共享(CORS)

flight turn 地理 add example html 變化 ring 否則

  通過 XHR 實現 Ajax 通信的一個主要限制,來源於跨域安全策略。默認情況下,XHR 對象只能訪 問與包含它的頁面位於同一個域中的資源。這種安全策略可以預防某些惡意行為。但是,實現合理的跨 域請求對開發某些瀏覽器應用程序也是至關重要的。

  CORS(Cross-Origin Resource Sharing,跨源資源共享)是 W3C 的一個工作草案,定義了在必須訪 問跨源資源時,瀏覽器與服務器應該如何溝通。CORS 背後的基本思想,就是使用自定義的 HTTP 頭部 讓瀏覽器與服務器進行溝通,從而決定請求或響應是應該成功,還是應該失敗。

  比如一個簡單的使用 GET 或 POST 發送的請求,它沒有自定義的頭部,而主體內容是 text/plain。在 發送該請求

時,需要給它(請求)附加一個額外的 Origin 頭部,其中包含請求頁面的源信息(協議、域名和端 口),以便服務器根據這個頭部信息來決定是否給予響應。下面是 Origin 頭部的一個示例:

Origin: http://www.nczonline.net

  如果服務器認為這個請求可以接受,就在 Access-Control-Allow-Origin 頭部中回發相同的源信息(如果是公共資源,可以回發”*”)。例如:

Access-Control-Allow-Origin: http://www.nczonline.net

  如果沒有這個頭部,或者有這個頭部但源信息不匹配,瀏覽器就會駁回請求。正常情況下,瀏覽器 會處理請求。註意,請求和響應都不包含 cookie 信息。

IE對CORS的實現

微軟在 IE8 中引入了 XDR(XDomainRequest)類型。這個對象與 XHR 類似,但能實現安全可靠 的跨域通信。XDR 對象的安全機制部分實現了 W3C 的 CORS 規範。以下是 XDR 與 XHR 的一些不同之 處。

  • cookie 不會隨請求發送,也不會隨響應返回。
  • 只能設置請求頭部信息中的 Content-Type 字段。
  • 不能訪問響應頭部信息。
  • 只支持GET和POST請求。

這些變化使 CSRF(Cross-Site Request Forgery,跨站點請求偽造)XSS(Cross-Site Scripting,跨 站點腳本)的問題得到了緩解。被請求的資源可以根據它認為合適的任意數據(用戶代理、來源頁面等) 來決定是否設置 Access-Control- Allow-Origin 頭部。作為請求的一部分

,Origin 頭部的值表示 請求的來源域,以便遠程資源明確地識別 XDR 請求。

XDR 對象的使用方法與 XHR 對象非常相似。也是創建一個 XDomainRequest 的實例,調用 open() 方法,再調用 send()方法。但與 XHR 對象的 open()方法不同,XDR 對象的 open()方法只接收兩個 參數:請求的類型和 URL。

所有 XDR 請求都是異步執行的,不能用它來創建同步請求。請求返回之後,會觸發 load 事件, 響應的數據也會保存在 responseText 屬性中,如下所示。

var xdr = new XDomainRequest();
xdr.onload = function(){
    alert(xdr.responseText);
};
xdr.open("get", "http://www.somewhere-else.com/page/");
xdr.send(null);

在接收到響應後,你只能訪問響應的原始文本;沒有辦法確定響應的狀態代碼。而且,只要響應有 效就會觸發 load 事件,如果失敗(包括響應中缺少 Access-Control-Allow-Origin 頭部)就會觸 發 error 事件。遺憾的是,除了錯誤本身之外,沒有其他信息可用,因此唯一能夠確定的就只有請求 未成功了。要檢測錯誤,可以像下面這樣指定一個 onerror 事件處理程序。

var xdr = new XDomainRequest();
xdr.onload = function(){
    alert(xdr.responseText);
};
?xdr.onerror = function(){ 5 alert("An error occurred.");
};
xdr.open("get","SomeUrl");
xdr.send(null);

??鑒於導致 XDR 請求失敗的因素很多,因此建議你不要忘記通過 onerror 事件處 理程序來捕獲該事件;否則,即使請求失敗也不會有任何提示。

在請求返回前調用 abort()方法可以終止請求:

xdr.abort(); //終止請求

與 XHR 一樣,XDR 對象也支持 timeout 屬性以及 ontimeout 事件處理程序。下面是一個例子。

var xdr = new XDomainRequest();
xdr.onload = function(){
XDomainRequestExample01.htm
??????alert(xdr.responseText);
}; 10 xdr.onerror = function(){
?    alert("An error occurred.");
};
xdr.timeout = 1000;
xdr.ontimeout = function(){
    alert("Request took too long.");
};
xdr.open("get", "http://www.somewhere-else.com/page/");
xdr.send(null);

這個例子會在運行 1 秒鐘後超時,並隨即調用 ontimeout 事件處理程序。

?為支持 POST 請求,XDR 對象提供了 contentType 屬性,用來表示發送數據的格式,如下面的例子所示。

var xdr = new XDomainRequest();
xdr.onload = function(){
    alert(xdr.responseText);
};
xdr.onerror = function(){
    alert("An error occurred.");
};
xdr.open("post", "http://www.somewhere-else.com/page/");
xdr.contentType = "application/x-www-form-urlencoded";
xdr.send("name1=value1&name2=value2");

這個屬性是通過 XDR 對象影響頭部信息的唯一方式。

其他瀏覽器對CORS的實現

Firefox 3.5+、Safari 4+、Chrome、iOS 版 Safari 和 Android 平臺中的 WebKit 都通過 XMLHttpRequest 對象實現了對 CORS 的原生支持。在嘗試打開不同來源的資源時,無需額外編寫代碼就可以觸發這個行 為。要請求位於另一個域中的資源,使用標準的 XHR 對象並在 open()方法中傳入絕對 URL 即可,例如:

var xhr = createXHR();
xhr.onreadystatechange = function(){
        if (xhr.readyState == 4){
            if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
                alert(xhr.responseText);
            } else {
                alert("Request was unsuccessful: " + xhr.status);
            }
} };
xhr.open("get", "http://www.somewhere-else.com/page/", true);
xhr.send(null);

與 IE 中的 XDR 對象不同,通過跨域 XHR 對象可以訪問 status 和 statusText 屬性,而且還支 持同步請求。跨域 XHR 對象也有一些限制,但為了安全這些限制是必需的。以下就是這些限制。

  • 不能使用 setRequestHeader()設置自定義頭部。
  • 不能發送和接收 cookie。
  • 調用 getAllResponseHeaders()方法總會返回空字符串。

由於無論同源請求還是跨源請求都使用相同的接口,因此對於本地資源,最好使用相對 URL,在訪 問遠程資源時再使用絕對 URL。這樣做能消除歧義,避免出現限制訪問頭部或本地 cookie 信息等問題。

Preflighted Reqeusts

CORS 通過一種叫做 Preflighted Requests 的透明服務器驗證機制支持開發人員使用自定義的頭部、 GET 或 POST 之外的方法,以及不同類型的主體內容。在使用下列高級選項來發送請求時,就會向服務 器發送一個 Preflight 請求。這種請求使用 OPTIONS 方法,發送下列頭部。

  • Origin:與簡單的請求相同。
  • Access-Control-Request-Method:請求自身使用的方法。
  • Access-Control-Request-Headers:(可選)自定義的頭部信息,多個頭部以逗號分隔。

以下是一個帶有自定義頭部 NCZ 的使用 POST 方法發送的請求。

Origin: http://www.nczonline.net
Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ

發送這個請求後,服務器可以決定是否允許這種類型的請求。服務器通過在響應中發送如下頭部與 瀏覽器進行溝通。

  • Access-Control-Allow-Origin:與簡單的請求相同。
  • Access-Control-Allow-Methods:允許的方法,多個方法以逗號分隔。
  • Access-Control-Allow-Headers:允許的頭部,多個頭部以逗號分隔。
  • Access-Control-Max-Age:應該將這個 Preflight 請求緩存多長時間(以秒表示)。

例如:

Access-Control-Allow-Origin: http://www.nczonline.net
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Headers: NCZ
Access-Control-Max-Age: 1728000

Preflight 請求結束後,結果將按照響應中指定的時間緩存起來。而為此付出的代價只是第一次發送 這種請求時會多一次 HTTP 請求。

支持 Preflight 請求的瀏覽器包括 Firefox 3.5+、Safari 4+和 Chrome。IE 10 及更早版本都不支持。

帶憑據的請求

默認情況下,跨源請求不提供憑據(cookie、HTTP 認證及客戶端 SSL 證明等)。通過將 withCredentials 屬性設置為 true,可以指定某個請求應該發送憑據。如果服務器接受帶憑據的請 求,會用下面的 HTTP 頭部來響應。

Access-Control-Allow-Credentials: true

如果發送的是帶憑據的請求,但服務器的響應中沒有包含這個頭部,那麽瀏覽器就不會把響應交給JavaScript(於是,responseText 中將是空字符串,status 的值為 0,而且會調用 onerror()事件處 理程序)。另外,服務器還可以在 Preflight 響應中發送這個 HTTP 頭部,表示允許源發送帶憑據的請求。

跨瀏覽器的CORS

即使瀏覽器對 CORS 的支持程度並不都一樣,但所有瀏覽器都支持簡單的(非 Preflight 和不帶憑據 的)請求,因此有必要實現一個跨瀏覽器的方案。檢測 XHR 是否支持 CORS 的最簡單方式,就是檢查 是否存在 withCredentials 屬性。再結合檢測 XDomainRequest 對象是否存在,就可以兼顧所有瀏 覽器了。

?function createCORSRequest(method, url){
    var xhr = new XMLHttpRequest();
    if ("withCredentials" in xhr){
        xhr.open(method, url, true);
    } else if (typeof XDomainRequest != "undefined"){
?        xhr = new XDomainRequest();
        xhr.open(method, url);
    } else {
       xhr = null;
    }
        return xhr;
}

var request = createCORSRequest("get", "http://www.somewhere-else.com/page/"); 
if (request){
    request.onload = function(){
        // 對request.responseText 進行處理
    };
    request.send();
}

Firefox、Safari 和 Chrome 中的 XMLHttpRequest 對象與 IE 中的 XDomainRequest 對象類似,都 提供了夠用的接口,因此以上模式還是相當有用的。這兩個對象共同的屬性/方法如下。

  • abort():用於停止正在進行的請求。
  • onerror:用於替代 onreadystatechange 檢測錯誤。 ? onload:用於替代 onreadystatechange 檢測成功。 - responseText:用於取得響應內容。
  • send():用於發送請求。

以上成員都包含在 createCORSRequest()函數返回的對象中,在所有瀏覽器中都能正常使用。

其他跨域技術

在 CORS 出現以前,要實現跨域 Ajax 通信頗費一些周折。開發人員想出了一些辦法,利用 DOM 中 能夠執行跨域請求的功能,在不依賴 XHR 對象的情況下也能發送某種請求。雖然 CORS 技術已經無處 不在,但開發人員自己發明的這些技術仍然被廣泛使用,畢竟這樣不需要修改服務器端代碼。

圖像Ping

上述第一種跨域請求技術是使用<img>標簽。我們知道,一個網頁可以從任何網頁中加載圖像,不 用擔心跨域不跨域。這也是在線廣告跟蹤瀏覽量的主要方式。正如第 13 章討論過的,也可以動態地創 建圖像,使用它們的 onload 和 onerror 事件處理程序來確定是否接收到了響應。

動態創建圖像經常用於圖像 Ping。圖像 Ping 是與服務器進行簡單、單向的跨域通信的一種方式。 請求的數據是通過查詢字符串形式發送的,而響應可以是任意內容,但通常是像素圖或 204 響應。通過 圖像 Ping,瀏覽器得不到任何具體的數據,但通過偵聽 load 和 error 事件,它能知道響應是什麽時 候接收到的。來看下面的例子。

var img = new Image();
img.onload = img.onerror = function(){
    alert("Done!");
};
img.src="https://gss0.baidu.com/7051cy89RMgCncy6lo7D0j9wexYrbOWh7c50/pi-loading.png" data-src = "http://www.example.com/test?name=Nicholas";

這裏創建了一個 Image 的實例,然後將 onload 和 onerror 事件處理程序指定為同一個函數。這 樣無論是什麽響應,只要請求完成,就能得到通知。請求從設置 src="https://gss0.baidu.com/7051cy89RMgCncy6lo7D0j9wexYrbOWh7c50/pi-loading.png" data-src 屬性那一刻開始,而這個例子在請 求中發送了一個 name 參數。

圖像 Ping 最常用於跟蹤用戶點擊頁面或動態廣告曝光次數。圖像 Ping 有兩個主要的缺點,一是只 能發送 GET 請求,二是無法訪問服務器的響應文本。因此,圖像 Ping 只能用於瀏覽器與服務器間的單向通信。

JSONP

JSONP 是 JSON with padding(填充式 JSON 或參數式 JSON)的簡寫,是應用 JSON 的一種新方法, 在後來的 Web 服務中非常流行。JSONP 看起來與 JSON 差不多,只不過是被包含在函數調用中的 JSON, 4就像下面這樣。

callback({ "name": "Nicholas" });

JSONP 由兩部分組成:回調函數和數據。回調函數是當響應到來時應該在頁面中調用的函數。回調 函數的名字一般是在請求中指定的。而數據就是傳入回調函數中的 JSON 數據。下面是一個典型的 JSONP 請求。

http://freegeoip.net/json/?callback=handleResponse

這個 URL 是在請求一個 JSONP 地理定位服務。通過查詢字符串來指定 JSONP 服務的回調參數是很 常見的,就像上面的 URL 所示,這裏指定的回調函數的名字叫 handleResponse()。

JSONP 是通過動態<script>元素來使用的,使用時可以為src屬性指定一個跨域 URL。這裏的<script>元素與<img>元素類似,都有能力不受限制地從其他域 加載資源。因為 JSONP 是有效的 JavaScript 代碼,所以在請求完成後,即在 JSONP 響應加載到頁面中 以後,就會立即執行。來看一個例子。 ?

function handleResponse(response){
alert("You’re at IP address " + response.ip + ", which is in " +
          response.city + ", " + response.region_name);
}
var script = document.createElement("script");
script.src="http://freegeoip.net/json/?callback=handleResponse"; 
document.body.insertBefore(script, document.body.firstChild);

這個例子通過查詢地理定位服務來顯示你的 IP 地址和位置信息。
JSONP 之所以在開發人員中極為流行,主要原因是它非常簡單易用。與圖像 Ping 相比,它的優點 在於能夠直接訪問響應文本,支持在瀏覽器與服務器之間雙向通信。不過,JSONP 也有兩點不足。

    • 首先,JSONP 是從其他域中加載代碼執行。如果其他域不安全,很可能會在響應中夾帶一些惡意代碼,而此時除了完全放棄 JSONP 調用之外,沒有辦法追究。因此在使用不是你自己運維的 Web 服務時, 一定得保證它安全可靠。

    • 其次,要確定 JSONP 請求是否失敗並不容易。雖然 HTML5 給<script>元素新增了一個 onerror 事件處理程序,但目前還沒有得到任何瀏覽器支持。為此,開發人員不得不使用計時器檢測指定時間內是否接收到了響應。但就算這樣也不能盡如人意,畢竟不是每個用戶上網的速度和帶寬都一樣。

跨域資源共享(CORS)