1. 程式人生 > >前端跨域的幾種方式

前端跨域的幾種方式

前言

受瀏覽器同源策略的限制,本域的js不能操作其他域的頁面物件(比如DOM)。但在安全限制的同時也給注入iframe或是ajax應用上帶來了不少麻煩。所以我們要通過一些方法使本域的js能夠操作其他域的頁面物件或者使其他域的js能操作本域的頁面物件(iframe之間)。


這裡需要明確的一點是:所謂的域跟js的存放伺服器沒有關係,比如baidu.com的頁面載入了google.com的js,那麼此js的所在域是baidu.com而不是google.com。也就是說,此時該js能操作baidu.com的頁面物件,而不能操作google.com的頁面物件。

跨域的方法總結

單向跨域(一般用於獲取資料)


一、使用JSONP跨域



原理:因為通過script標籤引入的js是不受同源策略的限制的(正如前文提到的baidu.com的頁面載入了google.com的js)。所以我們可以通過script標籤引入一個js或者是一個其他字尾形式(如PHP,jsp等)的檔案,此檔案返回一個js函式的呼叫,如返回JSONP_getUsers(["paco","john","lili"]),也就是說此檔案返回的結果呼叫了JSONP_getUsers函式,並且把["paco","john","lili"]傳進去,這個["paco","john","lili"]是一個使用者列表。那麼如果此時我們的頁面中有一個JSONP_getUsers函式,那麼JSONP_getUsers就被呼叫到,並且傳入了使用者列表。此時就實現了在本域獲取其他域資料的功能,也就是跨域。

實現例子如下:

前端引入遠端js並定義好JSONP_getUsers函式,注意需要先定義好JSONP_getUsers函式,避免在遠端js載入完成並呼叫JSONP_getUsers時,此函式不存在:

//本域為baidu.com  
<script>  
    function JSONP_getUsers(users){  
        console.dir(users);  
    }  
</script>  
//載入google.com的getUsers.php  
<script src="http://www.google.com/getUsers.php"></script>
需要google.com提供支援,getUsers.php程式碼如下:
<?php>  
    echo 'JSONP_getUsers(["paco","john","lili"])';//返回一個js函式的呼叫  
?>
JSONP易於實現,但是也會存在一些安全隱患,如果第三方的指令碼隨意地執行,那麼它就可以篡改頁面內容,截獲敏感資料。但是在受信任的雙方傳遞資料,JSONP是非常合適的選擇。可以看出來JSONP跨域一般用於獲取其他域的資料。


一般能夠用JSONP實現跨域就用JSONP實現,這也是前端用的最多的跨域方法。


二、動態建立script標籤


這種方法其實是JSONP跨域的簡化版,JSONP只是在此基礎上加入了回撥函式。

比如上例中的getUsers.php返回的如果不是一個js函式的呼叫,而是一個js變數,如:

<?php>  
    echo 'var users=["paco","john","lili"]';//返回一個js變數users  
?>
三、flash URLLoader


flash有自己的一套安全策略,伺服器可以通過crossdomain.xml檔案來宣告能被哪些域的SWF檔案訪問,SWF也可以通過API來確定自身能被哪些域的SWF載入。當跨域訪問資源時,例如從域baidu.com請求域google.com上的資料,我們可以藉助flash來發送HTTP請求。首先,修改域google.com上的crossdomain.xml(一般存放在根目錄,如果沒有需要手動建立) ,把baidu.com加入到白名單。其次,通過Flash URLLoader傳送HTTP請求,最後,通過Flash API把響應結果傳遞給JavaScript。Flash URLLoader是一種很普遍的跨域解決方案,不過需要支援iOS的話,這個方案就不可行了。


四、Access Control


此跨域方法目前只在很少的瀏覽器中得以支援,這些瀏覽器可以傳送一個跨域的HTTP請求(Firefox, Google Chrome等通過XMLHTTPRequest實現,IE8下通過XDomainRequest實現),請求的響應必須包含一個Access- Control-Allow-Origin的HTTP響應頭,該響應頭聲明瞭請求域的可訪問許可權。例如baidu.com對google.com下的getUsers.php傳送了一個跨域的HTTP請求(通過ajax),那麼getUsers.php必須加入如下的響應頭:
header("Access-Control-Allow-Origin: http://www.baidu.com");//表示允許baidu.com跨域請求本檔案

五、window.name

window 物件的name屬性是一個很特別的屬性,當該window的location變化,然後重新載入,它的name屬性可以依然保持不變。那麼我們可以在頁面 A中用iframe載入其他域的頁面B,而頁面B中用JavaScript把需要傳遞的資料賦值給window.name,iframe載入完成之後(iframe.onload),頁面A修改iframe的地址,將其變成同域的一個地址,然後就可以讀出iframe的window.name的值了(因為A中的window.name和iframe中的window.name互相獨立的,所以不能直接在A中獲取window.name,而要通過iframe獲取其window.name)。這個方式非常適合單向的資料請求,而且協議簡單、安全。不會像JSONP那樣不做限制地執行外部指令碼。

六、伺服器代理

在資料提供方沒有提供對JSONP協議或者 window.name協議的支援,也沒有對其它域開放訪問許可權時,我們可以通過server proxy的方式來抓取資料。例如當baidu.com域下的頁面需要請求google.com下的資原始檔getUsers.php時,直接傳送一個指向 google.com/getUsers.php的Ajax請求肯定是會被瀏覽器阻止。這時,我們在baidu.com下配一個代理,然後把Ajax請求繫結到這個代理路徑下,例如baidu.com/proxy/, 然後這個代理髮送HTTP請求訪問google.com下的getUsers.php,跨域的HTTP請求是在伺服器端進行的(伺服器端沒有同源策略限制),客戶端並沒有產生跨域的Ajax請求。這個跨域方式不需要和目標資源簽訂協議,帶有侵略性。

雙向跨域(兩個iframe之間或者兩個頁面之間,一般用於獲取對方資料,document.domain方式還可以直接操作對方DOM)

七、document.domain(兩個iframe之間)

通過修改document的domain屬性,我們可以在域和子域或者不同的子域之間通訊。同域策略認為域和子域隸屬於不同的域,比如baidu.com和 youxi.baidu.com是不同的域,這時,我們無法在baidu.com下的頁面中呼叫youxi.baidu.com中定義的JavaScript方法。但是當我們把它們document的domain屬性都修改為baidu.com,瀏覽器就會認為它們處於同一個域下,那麼我們就可以互相獲取對方資料或者操作對方DOM了。

問題:

1、安全性,當一個站點被攻擊後,另一個站點會引起安全漏洞。

2、如果一個頁面中引入多個iframe,要想能夠操作所有iframe,必須都得設定相同domain。

八、location.hash(兩個iframe之間),又稱FIM,Fragment Identitier Messaging的簡寫

因為父視窗可以對iframe進行URL讀寫,iframe也可以讀寫父視窗的URL,URL有一部分被稱為hash,就是#號及其後面的字元,它一般用於瀏覽器錨點定位,Server端並不關心這部分,應該說HTTP請求過程中不會攜帶hash,所以這部分的修改不會產生HTTP請求,但是會產生瀏覽器歷史記錄。此方法的原理就是改變URL的hash部分來進行雙向通訊。每個window通過改變其他 window的location來發送訊息(由於兩個頁面不在同一個域下IE、Chrome不允許修改parent.location.hash的值,所以要藉助於父視窗域名下的一個代理iframe),並通過監聽自己的URL的變化來接收訊息。這個方式的通訊會造成一些不必要的瀏覽器歷史記錄,而且有些瀏覽器不支援onhashchange事件,需要輪詢來獲知URL的改變,最後,這樣做也存在缺點,諸如資料直接暴露在了url中,資料容量和型別都有限等。下面舉例說明:

假如父頁面是baidu.com/a.html,iframe嵌入的頁面為google.com/b.html(此處省略了域名等url屬性),要實現此兩個頁面間的通訊可以通過以下方法。

1、a.html傳送資料到b.html

(1) a.html下修改iframe的src為google.com/b.html#paco

(2) b.html監聽到url發生變化,觸發相應操作

2、b.html傳送資料到a.html,由於兩個頁面不在同一個域下IE、Chrome不允許修改parent.location.hash的值,所以要藉助於父視窗域名下的一個代理iframe

(1) b.html下建立一個隱藏的iframe,此iframe的src是baidu.com域下的,並掛上要傳送的hash資料,如src="http://www.baidu.com/proxy.html#data"

(2) proxy.html監聽到url發生變化,修改a.html的url(因為a.html和proxy.html同域,所以proxy.html可修改a.html的url hash)

(3) a.html監聽到url發生變化,觸發相應操作

b.html頁面的關鍵程式碼如下:

try {  
    parent.location.hash = 'data';  
} catch (e) {  
    // ie、chrome的安全機制無法修改parent.location.hash,  
    var ifrproxy = document.createElement('iframe');  
    ifrproxy.style.display = 'none';  
    ifrproxy.src = "http://www.baidu.com/proxy.html#data";  
    document.body.appendChild(ifrproxy);  
}
proxy.html頁面的關鍵程式碼如下:

    //因為parent.parent(即baidu.com/a.html)和baidu.com/proxy.html屬於同一個域,所以可以改變其location.hash的值  
    parent.parent.location.hash = self.location.hash.substring(1);  
九、使用HTML5的postMessage方法(兩個iframe之間或者兩個頁面之間)

高階瀏覽器Internet Explorer 8+, chrome,Firefox , Opera  和 Safari 都將支援這個功能。這個功能主要包括接受資訊的"message"事件和傳送訊息的"postMessage"方法。比如baidu.com域的A頁面通過iframe嵌入了一個google.com域的B頁面,可以通過以下方法實現A和B的通訊


A頁面通過postMessage方法傳送訊息:
window.onload = function() {  
    var ifr = document.getElementById('ifr');  
    var targetOrigin = "http://www.google.com";  
    ifr.contentWindow.postMessage('hello world!', targetOrigin);  
};
postMessage的使用方法:


otherWindow.postMessage(message, targetOrigin);


otherWindow:   指目標視窗,也就是給哪個window發訊息,是 window.frames 屬性的成員或者由 window.open 方法建立的視窗

message:   是要傳送的訊息,型別為 String、Object (IE8、9 不支援)

targetOrigin:   是限定訊息接收範圍,不限制請使用 '*'


B頁面通過message事件監聽並接受訊息:
var onmessage = function (event) {  
  var data = event.data;//訊息  
  var origin = event.origin;//訊息來源地址  
  var source = event.source;//源Window物件  
  if(origin=="http://www.baidu.com"){  
console.log(data);//hello world!  
  }  
};  
if (typeof window.addEventListener != 'undefined') {  
  window.addEventListener('message', onmessage, false);  
} else if (typeof window.attachEvent != 'undefined') {  
  //for ie  
  window.attachEvent('onmessage', onmessage);  
}

同理,也可以B頁面傳送訊息,然後A頁面監聽並接受訊息。


總結


跨域的方法很多,不同的應用場景我們都可以找到一個最合適的解決方案。比如單向的資料請求,我們應該優先選擇JSONP或者window.name,雙向通訊優先採取location.hash,在未與資料提供方達成通訊協議的情況下我們也可以用server proxy的方式來抓取資料。