1. 程式人生 > >前端跨域請求解決方案彙總

前端跨域請求解決方案彙總

同源策略限制從一個源載入的文件或指令碼如何與來自另一個源的資源進行互動。這是一個用於隔離潛在惡意檔案的關鍵的安全機制。但是有時候跨域請求資源是合理的需求,本文嘗試從多篇文章中彙總至今存在的所有跨域請求解決方案。

跨域請求

首先需要了解的是同源和跨源的概念。對於相同源,其定義為:如果協議、埠(如果指定了一個)和主機對於兩個頁面是相同的,則兩個頁面具有相同的源。只要三者之一任意一點有不同,那麼就為不同源。當一個資源從與該資源本身所在的伺服器的域或埠不同的域或不同的埠請求一個資源時,資源會發起一個跨域 HTTP 請求。而有關跨域請求受到限制的原因可以參考如下 MDN 文件片段:

跨域不一定是瀏覽器限制了發起跨站請求,而也可能是跨站請求可以正常發起,但是返回結果被瀏覽器攔截了。最好的例子是 CSRF 跨站攻擊原理,請求是傳送到了後端伺服器無論是否跨域!注意:有些瀏覽器不允許從 HTTPS 的域跨域訪問 HTTP,比如 Chrome 和 Firefox,這些瀏覽器在請求還未發出的時候就會攔截請求,這是一個特例。

解決方法彙總

以下我們由簡及深介紹各種存在的跨域請求解決方案,包括 document.domain, location.hash, window.name, window.postMessage, JSONP, WebSocket, CORS 。

document.domain

document.domain 的作用是用來獲取/設定當前文件的原始域部分,例如:

// 對於文件 www.example.xxx/good.html
document.domain=”www.example.xxx”

// 對於URI http://developer.mozilla.org/en/docs/DOM
document.domain=”developer.mozilla.org”
如果當前文件的域無法識別,那麼 domain 屬性會返回 null。

在根域範圍內,Mozilla允許你把domain屬性的值設定為它的上一級域。例如,在 developer.mozilla.org 域內,可以把domain設定為 “mozilla.org” 但不能設定為 “mozilla.com” 或者”org”。

因此,若兩個源所用協議、埠一致,主域相同而二級域名不同的話,可以借鑑該方法解決跨域請求。

document.domain = “github.io”
那麼之後頁面對 github.io 發起請求時頁面則會成功通過對 github.io 的同源檢測。比較直接的一個操作是,當我們在 a.github.io 頁面中利用 iframe 去載入 github.io 時,通過如上的賦值後,我們可以在 a.github.io 頁面中去操作 iframe 裡的內容。

我們同時考慮另一種情況:存在兩個子域名 a.github.io 以及 b.github.io , 其中前者域名下網頁 a.html 通過 iframe 引入了後者域名下的 b.html,此時在 a.html 中是無法直接操作 b.html 的內容的。

同樣利用 document.domain ,我們在兩個頁面中均加入

document.domain=’github.io’
這樣在以上的 a.html 中就可以操作通過 iframe 引入的 b.html 了。

document.domain的優點在於解決了主語相同的跨域請求,但是其缺點也是很明顯的:比如一個站點受到攻擊後,另一個站點會因此引起安全漏洞;若一個頁面中引入多個 iframe,想要操作所有的 iframe 則需要設定相同的 domain。

location.hash

location.hash 是一個可讀可寫的字串,該字串是 URL 的錨部分(從 # 號開始的部分)。例如:

// 對於頁面 http://example.com:1234/test.htm#part2
location.hash = “#part2”
同時,由於我們知道改變 hash 並不會導致頁面重新整理,所以可以利用 hash 在不同源間傳遞資料。

假設 github.io 域名下 a.html 和 shaonian.eu 域名下 b.html 存在跨域請求,那麼利用 location.hash 的一個解決方案如下:

a.html 頁面中建立一個隱藏的 iframe, src 指向 b.html,其中 src 中可以通過 hash 傳入引數給 b.html
b.html 頁面在處理完傳入的 hash 後通過修改 a.html 的 hash 值達到將資料傳送給 a.html 的目的
a.html 頁面新增一個定時器,每隔一定時間判斷自身的 location.hash 是否變化,以此響應處理
以上步驟中需要注意第二點:如何在 iframe 頁面中修改 父親頁面的 hash 值。由於在 IE 和 Chrome 下,兩個不同域的頁面是不允許 parent.location.hash 這樣賦值的,所以對於這種情況,我們需要在父親頁面域名下新增另一個頁面來實現跨域請求,具體如下:

假設 a.html 中 iframe 引入了 b.html, 資料需要在這兩個頁面之間傳遞,且 c.html 是一個與 a.html 同源的頁面
a.html 通過 iframe 將資料通過 hash 傳給 b.html
b.html 通過 iframe 將資料通過 hash 傳給 c.html
c.html 通過 parent.parent.location.hash 設定 a.html 的 hash 達到傳遞資料的目的
location.bash方法的優點在於可以解決域名完全不同的跨域請求,並且可以實現雙向通訊;而缺點則包括以下幾點:

利用這種方法傳遞的資料量受到 url 大小的限制,傳遞資料型別有限
由於資料直接暴露在 url 中則存在安全問題
若瀏覽器不支援 onhashchange 事件,則需要通過輪訓來獲知 url 的變化
有些瀏覽器會在 hash 變化時產生歷史記錄,因此可能影響使用者體驗
window.name

該屬性用於獲取/設定視窗的名稱。其特徵在於:一個視窗的生命週期內,視窗載入的所有頁面共享該值,且都具有對該屬性的讀寫許可權。這意味著如果不修改該值,那麼在不同頁面載入之後該值也不會變,且其支援長達 2MB 的儲存量。

利用該特性我們可以將跨域請求用如下步驟解決:

在 a.github.io/a.html 中建立 iframe 指向 b.github.io/b.html (頁面會將自身的 window.name 附在 iframe 上)
給 a.github.io/a.html 新增監聽 iframe 的 onload 事件,在該事件中將 iframe 的 src 設定為本地域的代理檔案(代理檔案和a.html處於同一域下,可以相互通訊),同時可以傳出 iframe 的 name 值
獲取資料後銷燬 iframe,釋放記憶體,同時也保證了安全
window.name的優勢在於巧妙地繞過了瀏覽器的跨域訪問限制,但同時它又是安全操作。

window.postMessage

HTML5 為了解決這個問題,引入了一個全新的 API:跨文件通訊 API(Cross-document messaging)。這個 API 為 window 物件新增了一個 window.postMessage 方法,允許跨視窗通訊,不論這兩個視窗是否同源。

API 的詳細使用方法請見 MDN 。

JSONP

JSONP, 全稱 JSON with Padding,是使用 AJAX 實現的請求不同源的跨域。其基本原理:網頁通過新增一個

// 當前頁面 a.com/a.html
<script type="text/javascript">
//回撥函式
function callback(data) {
    alert(data.message);
}
</script>
<script type="text/javascript" src="http://b.com/test.js"></script>

// test.js
// 呼叫callback函式,並以json資料形式作為闡述傳遞,完成回撥
callback({message:"success"});

為了保證 script 的靈活,我們可以通過 JavaScript 動態建立 script 標籤,並通過 HTTP 引數向伺服器傳入回撥函式名,案例如下所示:

<script type="text/javascript">
    // 新增<script>標籤的方法
    function addScriptTag(src){
        var script = document.createElement('script');
        script.setAttribute("type","text/javascript");
        script.src = src;
        document.body.appendChild(script);
    }

    window.onload = function(){
        // 搜尋apple,將自定義的回撥函式名result傳入callback引數中
        addScriptTag("http://ajax.googleapis.com/ajax/services/search/web?v=1.0&q=apple&callback=result");

    }
    // 自定義的回撥函式result
    function result(data) {
        // 我們就簡單的獲取apple搜尋結果的第一條記錄中url資料
        alert(data.responseData.results[0].unescapedUrl);
    }
</script>

jQuery 有相應的 JSONP 的實現方法,見 API 。

JSONP的優點在於簡單適用,老式瀏覽器全部支援,伺服器改造小。不需要XMLHttpRequest或ActiveX的支援;但缺點是隻支援 GET 請求。

WebSocket

WebSocket 協議不實行同源政策,只要伺服器支援,就可以通過它進行跨源通訊。

CORS

CORS是一個W3C標準,全稱是”跨域資源共享”(Cross-origin resource sharing)。它允許瀏覽器向跨源伺服器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。

跨域資源共享( CORS )機制允許 Web 應用伺服器進行跨域訪問控制,從而使跨域資料傳輸得以安全進行。其需要服務端和客戶端同時支援。

跨域資源共享標準( cross-origin sharing standard )允許在下列場景中使用跨域 HTTP 請求:

由 XMLHttpRequest 或 Fetch 發起的跨域 HTTP 請求

Web 字型 (CSS 中通過 @font-face 使用跨域字型資源), 因此,網站就可以釋出 TrueType 字型資源,並只允許已授權網站進行跨站呼叫

WebGL 貼圖

使用 drawImage 將 Images/video 畫面繪製到 canvas

樣式表(使用 CSSOM)

Scripts (未處理的異常)

CORS 存在以下三種主要場景,分別是 簡單請求,預檢請求和附帶身份憑證的請求 。

簡單請求 :若只使用 GET, HEAD 或者 POST 請求,且除 CORS 安全的首部欄位集合外,無人為設定該集合之外的其他首部欄位,同時 Content-Type 值屬於下列之一,那麼該請求則可以被視為簡單請求:
application/x-www-form-urlencoded
multipart/form-data
text/plain
此情況下,若服務端返回的 Access-Control-Allow-Origin: * ,則表明該資源可以被任意外域訪問。若要指定僅允許來自某些域的訪問,需要將 * 設定為該域,例如:

Access-Control-Allow-Origin: http://foo.example
預檢請求 :與前述簡單請求不同,該要求必須首先使用 OPTIONS 方法發起一個預檢請求到伺服器,以獲知伺服器是否允許該實際請求。當請求滿足以下三個條件任意之一時, 即應首先發送預檢請求:
使用了 PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH 中任一的 HTTP 方法
人為設定了對 CORS 安全的首部欄位集合之外的其他首部欄位
Content-Type 的值不屬於下列之一
application/x-www-form-urlencoded
multipart/form-data
text/plain
預檢請求完成之後(通過 OPTIONS 方法實現),才傳送實際請求。一個示範 HTTP 請求如下所示:

var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/post-here/';
var body = '<?xml version="1.0"?><person><name>Arun</name></person>';

function callOtherDomain(){
  if(invocation)
    {
      invocation.open('POST', url, true);
      invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
      invocation.setRequestHeader('Content-Type', 'application/xml');
      invocation.onreadystatechange = handler;
      invocation.send(body); 
    }
}

附帶身份憑證的請求 :這種方式的特點在於能夠在跨域請求時向伺服器傳送憑證請求,例如 Cookies (withCredentials 標誌設定為 true)。
一般而言,對於跨域 XMLHttpRequest 或 Fetch 請求,瀏覽器不會發送身份憑證資訊。如果要傳送憑證資訊,需要設定 XMLHttpRequest 的某個特殊標誌位。但是需要注意的是,如果伺服器端的響應中未攜帶 Access-Control-Allow-Credentials: true ,瀏覽器將不會把響應內容返回給請求的傳送者。

附帶身份憑證的請求與萬用字元

對於附帶身份憑證的請求,伺服器不得設定 Access-Control-Allow-Origin 的值為“*”。

這是因為請求的首部中攜帶了 Cookie 資訊,如果 Access-Control-Allow-Origin 的值為“*”,請求將會失敗。而將 Access-Control-Allow-Origin 的值設定為 http://foo.example,則請求將成功執行。

另外,響應首部中也攜帶了 Set-Cookie 欄位,嘗試對 Cookie 進行修改。如果操作失敗,將會丟擲異常。

MDN 引例如下:

var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/credentialed-content/';

function callOtherDomain(){
  if(invocation) {
    invocation.open('GET', url, true);
    invocation.withCredentials = true;
    invocation.onreadystatechange = handler;
    invocation.send(); 
  }
}

其實由上我們知道, CORS 的優點也非常明顯:CORS支援所有型別的HTTP請求,是跨域HTTP請求的根本解決方案。

以上就是所有的跨域請求解決方案,根據實際生產環境,總有一款適合你。