JavaScript跨域方法總結

同源安全策略

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

一、CORS

Cross-Origin Resource Sharing,跨域資源共享

1、原理

CORS是 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 信息。

2、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 請求都是異步執行的,不能用它來創建同步請求。

3、其他瀏覽器對CORS的實現

通過 XMLHttpRequest對象實現了對 CORS 的原生支持。在嘗試打開不同來源的資源時,無需額外編寫代碼就可以觸發這個行爲。 要請求位於另一個域中的資源, 使用標準的 XHR對象並在 open() 方法中傳入絕對 URL即可。

xhr.open("get", "http://www.somewhere-else.com/page/", true);

與 IE 中的 XDR 對象不同,通過跨域 XHR 對象可以訪問 status 和 statusText 屬性,而且還支持同步請求。跨域 XHR 對象也有一些限制,但爲了安全這些限制是必需的。以下就是這些限制。
 不能使用 setRequestHeader() 設置自定義頭部。
 不能發送和接收 cookie。
 調用 getAllResponseHeaders() 方法總會返回空字符串。
由於無論同源請求還是跨源請求都使用相同的接口,因此對於本地資源,最好使用相對 URL,在訪問遠程資源時再使用絕對 URL。這樣做能消除歧義,避免出現限制訪問頭部或本地 cookie 信息等問題。

4、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 請求。

5、帶憑據的請求

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

Access-Control-Allow-Credentials: true

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

6、跨瀏覽器的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"){
        vxhr = 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() 函數返回的對象中,在所有瀏覽器中都能正常使用。

二、圖像ping

圖像 Ping 是與服務器進行簡單、單向的跨域通信的一種方式。

使用 img 標籤。請求的數據是通過查詢字符串形式發送的,而響應可以是任意內容,但通常是像素圖或 204 響應。通過圖像 Ping,瀏覽器得不到任何具體的數據,但通過偵聽 load 和 error 事件,它能知道響應是什麼時候接收到的。

var img = new Image();
img.onload = img.onerror = function(){
    alert("Done!");
};
img.src = "http://www.example.com/test?name=Nicholas";

這裏創建了一個 Image 的實例,然後將 onload 和 onerror 事件處理程序指定爲同一個函數。這樣無論是什麼響應,只要請求完成,就能得到通知。請求從設置 src 屬性那一刻開始,而這個例子在請求中發送了一個 name 參數。
圖像 Ping 最常用於跟蹤用戶點擊頁面或動態廣告曝光次數。圖像 Ping 有兩個主要的缺點,一是隻能發送 GET 請求,二是無法訪問服務器的響應文本。因此,圖像 Ping 只能用於瀏覽器與服務器間的單向通信。

三、JSONP

JSON with padding,填充式 JSON 或參數式 JSON。

JSONP 看起來與 JSON 差不多, 只不過是被包含在函數調用中的 JSON,就像下面這樣。

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 地址和位置信息。

優點

  1. 簡單易用;
  2. 能夠直接訪問響應文本,支持在瀏覽器與服務器之間雙向通信。

缺點

  1. JSONP 是從其他域中加載代碼執行。如果其他域不安全,很可能會在響應中夾帶一些惡意代碼,而此時除了完全放棄 JSONP 調用之外,沒有辦法追究。因此在使用不是你自己運維的 Web 服務時,一定得保證它安全可靠。
  2. 要確定 JSONP 請求是否失敗並不容易。雖然 HTML5 給 script 元素新增了一個 onerror事件處理程序,但目前還沒有得到任何瀏覽器支持。爲此,開發人員不得不使用計時器檢測指定時間內是否接收到了響應。但就算這樣也不能盡如意,畢竟不是每個用戶上網的速度和帶寬都一樣。

四、Comet

更高級的 Ajax 技術,經常也有人稱爲“服務器推送”。Ajax 是一種從頁面向服務器請求數據的技術,而 Comet 則是一種服務器向頁面推送數據的技術。Comet 能夠讓信息近乎實時地被推送到頁面上,非常適合處理體育比賽的分數和股票報價。
有兩種實現 Comet 的方式:長輪詢和流。

1、長輪詢

短輪詢:瀏覽器定時向服務器發送請求,看有沒有更新的數據。
長輪詢:傳統輪詢(也稱爲短輪詢)的一個翻版,即長輪詢把短輪詢顛倒了一下。頁面發起一個到服務器的請求,然後服務器一直保持連接打開,直到有數據可發送。發送完數據之後,瀏覽器關閉連接,隨即又發起一個到服務器的新請求。這一過程在頁面打開期間一直持續不斷。
短輪詢:
短輪詢
長輪詢:
長輪詢
無論是短輪詢還是長輪詢,瀏覽器都要在接收數據之前,先發起對服務器的連接。兩者最大的區別在於服務器如何發送數據。短輪詢是服務器立即發送響應,無論數據是否有效,而長輪詢是等待發送響應。輪詢的優勢是所有瀏覽器都支持,因爲使用 XHR 對象和 setTimeout() 就能實現。而你要做的就是決定什麼時候發送請求。

2、http流

流不同於上述兩種輪詢,因爲它在頁面的整個生命週期內只使用一個 HTTP 連接。具體來說,就是瀏覽器向服務器發送一個請求,而服務器保持連接打開,然後週期性地向瀏覽器發送數據。
下面這段 PHP 腳本就是採用流實現的服務器中常見的形式。

<?php
    $i = 0;
    while(true){
        //輸出一些數據,然後立即刷新輸出緩存
        echo "Number is $i";
        flush();

        //等幾秒鐘
        sleep(10);
        $i++;
    }
php>

所有服務器端語言都支持打印到輸出緩存然後刷新(將輸出緩存中的內容一次性全部發送到客戶端)的功能。而這正是實現 HTTP 流的關鍵所在。
在 Firefox、Safari、Opera 和 Chrome 中,通過偵聽 readystatechange 事件及檢測 readyState
的值是否爲 3,就可以利用 XHR 對象實現 HTTP 流。在上述這些瀏覽器中,隨着不斷從服務器接收數據, readyState 的值會週期性地變爲 3。當 readyState 值變爲 3 時, responseText 屬性中就會保存接收到的所有數據。此時,就需要比較此前接收到的數據,決定從什麼位置開始取得最新的數據。使用 XHR 對象實現 HTTP 流的典型代碼如下所示。

function createStreamingClient(url, progress, finished){
    var xhr = new XMLHttpRequest(),
    received = 0;
    xhr.open("get", url, true);
    xhr.onreadystatechange = function(){
        var result;
        if (xhr.readyState == 3){
            //只取得最新數據並調整計數器
            result = xhr.responseText.substring(received);
            received += result.length;
            //調用 progress 回調函數
            progress(result);
        } else if (xhr.readyState == 4){
            finished(xhr.responseText);
        }
    };
    xhr.send(null);
    return xhr;
}
var client = createStreamingClient("streaming.php", function(data){
    alert("Received: " + data);
}, function(data){
    alert("Done!");
});

這個 createStreamingClient() 函數接收三個參數:要連接的 URL、在接收到數據時調用的函數以及關閉連接時調用的函數。有時候,當連接關閉時,很可能還需要重新建立,所以關注連接什麼時候關閉還是有必要的。
只要 readystatechange 事件發生,而且readyState 值爲 3,就對 responseText 進行分割以取得最新數據。 這裏的 received 變量用於記錄已經處理了多少個字符, 每次 readyState 值爲 3 時都遞增。然後,通過 progress 回調函數來處理傳入的新數據。而當 readyState 值爲 4 時,則執行finished 回調函數,傳入響應返回的全部內容。

爲簡化Comet的兩個新接口:SSE、WebSockets

3、服務器發送事件【SSE】

SSE(Server-Sent Events,服務器發送事件)是圍繞只讀 Comet 交互推出的 API 或者模式。

SSE API用於創建到服務器的單向連接,服務器通過這個連接可以發送任意數量的數據。服務器響應的 MIME類型必須是 text/event-stream ,而且是瀏覽器中的 JavaScript API 能解析格式輸出。SSE 支持短輪詢、長輪詢和 HTTP 流,而且能在斷開連接時自動確定何時重新連接。

SSE API
SSE 的 JavaScript API 與其他傳遞消息的JavaScript API 很相似。要預訂新的事件流,首先要創建一個新的 EventSource 對象,並傳進一個入口點:

var source = new EventSource("myevents.php");

注意,傳入的 URL 必須與創建對象的頁面同源(相同的 URL 模式、域及端口) 。 EventSource 的實例有一個 readyState 屬性,值爲 0 表示正連接到服務器,值爲 1 表示打開了連接,值爲 2 表示關閉了連接。
另外,還有以下三個事件。
 open :在建立連接時觸發。
 message :在從服務器接收到新事件時觸發。
 error :在無法建立連接時觸發。
就一般的用法而言, onmessage 事件處理程序也沒有什麼特別的。

source.onmessage = function(event){
    var data = event.data;
    //處理數據
};

服務器發回的數據以字符串形式保存在 event.data 中。
默認情況下, EventSource 對象會保持與服務器的活動連接。如果連接斷開,還會重新連接。這就意味着 SSE 適合長輪詢和 HTTP 流。 如果想強制立即斷開連接並且不再重新連接, 可以調用close()方法。

source.close();

事件流
所謂的服務器事件會通過一個持久的 HTTP 響應發送,這個響應的 MIME 類型爲 text/event-stream 。響應的格式是純文本,最簡單的情況是每個數據項都帶有前綴 data: ,例如:

data: foo

data: bar

data: foo
data: bar

對以上響應而言,事件流中的第一個 message 事件返回的 event.data 值爲 “foo” ,第二個message 事件返回的 event.data 值爲 “bar” ,第三個 message 事件返回的 event.data 值爲”foo\nbar” (注意中間的換行符) 。對於多個連續的以 data: 開頭的數據行,將作爲多段數據解析,每個值之間以一個換行符分隔。只有在包含 data: 的數據行後面有空行時,纔會觸發 message 事件,因此在服務器上生成事件流時不能忘了多添加這一行。

通過 id: 前綴可以給特定的事件指定一個關聯的 ID,這個 ID 行位於 data: 行前面或後面皆可:

data: foo
id: 1

設置了 ID 後, EventSource 對象會跟蹤上一次觸發的事件。如果連接斷開,會向服務器發送一個包含名爲 Last-Event-ID 的特殊 HTTP 頭部的請求,以便服務器知道下一次該觸發哪個事件。在多次連接的事件流中,這種機制可以確保瀏覽器以正確的順序收到連接的數據段。

4、Web Sockets

Web Sockets的目標是在一個單獨的持久連接上提供全雙工、雙向通信。

在 JavaScript 中創建了 Web Socket 之後,會有一個 HTTP 請求發送到瀏覽器以發起連接。在取得服務器響應後,建立的連接會使用 HTTP 升級從 HTTP 協議交換爲 WebSocket 協議。也就是說,使用標準的 HTTP 服務器無法實現 Web Sockets,只有支持這種協議的專門服務器才能正常工作。
由於 Web Sockets使用了自定義的協議, 所以 URL 模式也略有不同。 未加密的連接不再是 http:// ,而是 ws:// ;加密的連接也不是 https:// ,而是 wss:// 。在使用 Web Socket URL 時,必須帶着這個模式,因爲將來還有可能支持其他模式。
使用自定義協議而非 HTTP 協議的好處
能夠在客戶端和服務器之間發送非常少量的數據,而不必擔心 HTTP 那樣字節級的開銷。由於傳遞的數據包很小,因此 Web Sockets非常適合移動應用。畢竟對移動應用而言,帶寬和網絡延遲都是關鍵問題。
缺點
制定協議的時間比制定JavaScript API 的時間還要長。Web Sockets曾幾度擱淺,就因爲不斷有人發現這個新協議存在一致性和安全性的問題。

4.1. Web Sockets API

要創建 Web Socket,先實例一個 WebSocket 對象並傳入要連接的 URL:

var socket = new WebSocket("ws://www.example.com/server.php");

注意,必須給 WebSocket 構造函數傳入絕對 URL。同源策略對 Web Sockets 不適用,因此可以通過它打開到任何站點的連接。至於是否會與某個域中的頁面通信,則完全取決於服務器。 (通過握手信息就可以知道請求來自何方。 )
實例化了 WebSocket 對象後,瀏覽器就會馬上嘗試創建連接。與 XHR 類似, WebSocket 也有一個表示當前狀態的 readyState 屬性。不過,這個屬性的值與 XHR 並不相同,而是如下所示。
 WebSocket.OPENING (0):正在建立連接。
 WebSocket.OPEN (1):已經建立連接。
 WebSocket.CLOSING (2):正在關閉連接。
 WebSocket.CLOSE (3):已經關閉連接。
WebSocket 沒有 readystatechange 事件; 不過, 它有其他事件, 對應着不同的狀態。 readyState的值永遠從 0 開始。

要關閉 Web Socket 連接,可以在任何時候調用 close() 方法。

socket.close();

調用了 close() 之後, readyState 的值立即變爲 2(正在關閉) ,而在關閉連接後就會變成 3。

4.2. 發送和接收數據

Web Socket 打開之後,就可以通過連接發送和接收數據。要向服務器發送數據,使用 send() 方法並傳入任意字符串,例如:

var socket = new WebSocket("ws://www.example.com/server.php");
socket.send("Hello world!");

因爲 Web Sockets只能通過連接發送純文本數據,所以對於複雜的數據結構,在通過連接發送之前,必須進行序列化。下面的例子展示了先將數據序列化爲一個 JSON 字符串,然後再發送到服務器:

var message = {
    time: new Date(),
    text: "Hello world!",
    clientId: "asdfp8734rew"
};
socket.send(JSON.stringify(message));

接下來,服務器要讀取其中的數據,就要解析接收到的 JSON 字符串。
當服務器向客戶端發來消息時, WebSocket 對象就會觸發 message 事件。這個 message 事件與其他傳遞消息的協議類似,也是把返回的數據保存在 event.data 屬性中。

socket.onmessage = function(event){
    var data = event.data;
    //處理數據
};

與通過 send() 發送到服務器的數據一樣, event.data 中返回的數據也是字符串。如果你想得到其他格式的數據,必須手工解析這些數據。

4.3. 其他事件

WebSocket 對象還有其他三個事件,在連接生命週期的不同階段觸發。
 open :在成功建立連接時觸發。
 error :在發生錯誤時觸發,連接不能持續。
 close :在連接關閉時觸發。
WebSocket 對象不支持 DOM 2 級事件偵聽器,因此必須使用 DOM 0 級語法分別定義每個事件處理程序。

var socket = new WebSocket("ws://www.example.com/server.php");
socket.onopen = function(){
    alert("Connection established.");
};
socket.onerror = function(){
    alert("Connection error.");
};
socket.onclose = function(){
    alert("Connection closed.");
};

在這三個事件中,只有 close 事件的 event 對象有額外的信息。這個事件的事件對象有三個額外的屬性: wasClean 、 code 和 reason 。其中, wasClean 是一個布爾值,表示連接是否已經明確地關閉; code 是服務器返回的數值狀態碼;而 reason 是一個字符串,包含服務器發回的消息。可以把這些信息顯示給用戶,也可以記錄到日誌中以便將來分析。

socket.onclose = function(event){
    console.log("Was clean? " + event.wasClean + " Code=" + event.code + " Reason="
+ event.reason);
};

5、SSE與Web Sockets

面對某個具體的用例,在考慮是使用 SSE 還是使用 Web Sockets 時,可以考慮如下幾個因素。
① 你是否有自由度建立和維護 Web Sockets服務器?因爲 Web Socket 協議不同於 HTTP,所以現有服務器
不能用於 Web Socket 通信。SSE 倒是通過常規 HTTP 通信,因此現有服務器就可以滿足需求。
② 到底需不需要雙向通信。如果用例只需讀取服務器數據(如比賽成績) ,那麼 SSE 比較容易實現。如果用例必須雙向通信(如聊天室) ,那麼 Web Sockets 顯然更好。別忘了,在不能選擇 Web Sockets 的情況下,組合 XHR 和 SSE 也是能實現雙向通信的。

5. document.domain

將頁面的document.domain設置爲相同的值,頁面間可以互相訪問對方的JavaScript對象。
注意:
不能將值設置爲URL中不包含的域;
鬆散的域名不能再設置爲緊繃的域名。

本文摘錄自《JavaScript高級程序設計(第3版)》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章