跨域資源共享(CORS)
通過 XHR 實現 Ajax 通信的一個主要限制,來源於跨域安全策略。默認情況下,XHR 對象只能訪 問與包含它的頁面位於同一個域中的資源。這種安全策略可以預防某些惡意行為。但是,實現合理的跨 域請求對開發某些瀏覽器應用程序也是至關重要的。
CORS(Cross-Origin Resource Sharing,跨源資源共享)是 W3C 的一個工作草案,定義了在必須訪 問跨源資源時,瀏覽器與服務器應該如何溝通。CORS 背後的基本思想,就是使用自定義的 HTTP 頭部 讓瀏覽器與服務器進行溝通,從而決定請求或響應是應該成功,還是應該失敗。
比如一個簡單的使用 GET 或 POST 發送的請求,它沒有自定義的頭部,而主體內容是 text/plain。在 發送該請求
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 頭部。作為請求的一部分
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)