1. 程式人生 > >前端跨域

前端跨域

hello 高級 php 綁定 () serve loaded oss cross

聲明:原文出自:http://blog.csdn.net/kongjiea/article/details/44201021

前言

受瀏覽器同源策略的限制,本域的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
  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
  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
  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
  1. <?php>
  2. echo ‘var users=["paco","john","lili"]‘;//返回一個js變量users
  3. ?>

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

[html] view plain copy
  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
  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
  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
  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
  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
  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的方式來抓取數據。

前端跨域