深入理解前端跨域方法和原理


前言

 

受瀏覽器同源策略的限制,本域的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時,此函數不存在:

 

[html]  view plain  copy
 
 print?在CODE上查看代碼片派生到我的代碼片
  1. //本域為baidu.com  
  2. <script>  
  3.     function JSONP_getUsers(users){  
  4.         console.dir(users);  
  5.     }  
  6. </script>  
  7. //加載google.com的getUsers.php  
  8. <script src="http://www.google.com/getUsers.php"></script>  

 

需要google.com提供支持,getUsers.php代碼如下:

 

[html]  view plain  copy
 
 print?在CODE上查看代碼片派生到我的代碼片
  1. <?php>  
  2.     echo 'JSONP_getUsers(["paco","john","lili"])';//返回一個js函數的調用  
  3. ?>  

 

為什么script標簽引入的文件不受同源策略的限制?因為script標簽引入的文件內容是不能夠被客戶端的js獲取到的,不會影響到被引用文件的安全,所以沒必要使script標簽引入的文件遵循瀏覽器的同源策略。而通過ajax加載的文件內容是能夠被客戶端js獲取到的,所以ajax必須遵循同源策略,否則被引入文件的內容會泄漏或者存在其他風險。

 

JSONP的缺點則是:它只支持GET請求而不支持POST等其它類型的HTTP請求(雖然采用post+動態生成iframe是可以達到post跨域的目的,但這樣做是一個比較極端的方式,不建議采用)。一般get請求能完成所有功能。比如如果需要給其他域服務器傳送參數可以在請求后掛參數(注意不要掛隱私數據),即

 

[html]  view plain  copy
 
 print?在CODE上查看代碼片派生到我的代碼片
  1. <script src="http://www.google.com/getUsers.php?flag=do&time=1"></script>。  

JSONP易於實現,但是也會存在一些安全隱患,如果第三方的腳本隨意地執行,那么它就可以篡改頁面內容,截獲敏感數據。但是在受信任的雙方傳遞數據,JSONP是非常合適的選擇。可以看出來JSONP跨域一般用於獲取其他域的數據。

 

 

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

 

二、動態創建script標簽

 

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

比如上例中的getUsers.php返回的如果不是一個js函數的調用,而是一個js變量,如:

 

[html]  view plain  copy
 
 print?在CODE上查看代碼片派生到我的代碼片
  1. <?php>  
  2.     echo 'var users=["paco","john","lili"]';//返回一個js變量users  
  3. ?>  

那么在本域下就可以取到data變量,這里需要注意判斷script節點是否加載完畢,如:

 

 

[html]  view plain  copy
 
 print?在CODE上查看代碼片派生到我的代碼片
  1. js.onload = js.onreadystatechange = function() {  
  2.     if (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete') {  
  3.         console.log(users);//此處取出其他域的數據  
  4.         js.onload = js.onreadystatechange = null;  
  5.     }  
  6. };  

 

三、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必須加入如下的響應頭:

 

[html]  view plain  copy
 
 print?在CODE上查看代碼片派生到我的代碼片
  1. 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頁面的關鍵代碼如下

 

[html]  view plain  copy
 
 print?在CODE上查看代碼片派生到我的代碼片
  1. try {  
  2.     parent.location.hash = 'data';  
  3. } catch (e) {  
  4.     // ie、chrome的安全機制無法修改parent.location.hash,  
  5.     var ifrproxy = document.createElement('iframe');  
  6.     ifrproxy.style.display = 'none';  
  7.     ifrproxy.src = "http://www.baidu.com/proxy.html#data";  
  8.     document.body.appendChild(ifrproxy);  
  9. }  

 

proxy.html頁面的關鍵代碼如下

 

[html]  view plain  copy
 
 print?在CODE上查看代碼片派生到我的代碼片
  1. //因為parent.parent(即baidu.com/a.html)和baidu.com/proxy.html屬於同一個域,所以可以改變其location.hash的值  
  2. 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方法發送消息:

 

[html]  view plain  copy
 
 print?在CODE上查看代碼片派生到我的代碼片
  1. window.onload = function() {  
  2.     var ifr = document.getElementById('ifr');  
  3.     var targetOrigin = "http://www.google.com";  
  4.     ifr.contentWindow.postMessage('hello world!', targetOrigin);  
  5. };  

 

postMessage的使用方法:

 

otherWindow.postMessage(message, targetOrigin);

 

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

message:   是要發送的消息,類型為 String、Object (IE8、9 不支持)

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

 

B頁面通過message事件監聽並接受消息:

 

[html]  view plain  copy
 
 print?在CODE上查看代碼片派生到我的代碼片
  1. var onmessage = function (event) {  
  2.   var data = event.data;//消息  
  3.   var origin = event.origin;//消息來源地址  
  4.   var source = event.source;//源Window對象  
  5.   if(origin=="http://www.baidu.com"){  
  6. console.log(data);//hello world!  
  7.   }  
  8. };  
  9. if (typeof window.addEventListener != 'undefined') {  
  10.   window.addEventListener('message', onmessage, false);  
  11. } else if (typeof window.attachEvent != 'undefined') {  
  12.   //for ie  
  13.   window.attachEvent('onmessage', onmessage);  
  14. }  

同理,也可以B頁面發送消息,然后A頁面監聽並接受消息。

 

 

總結

 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM