最近面試問的挺多的一個問題,就是JavaScript的跨域問題。在這里,對跨域的一些方法做個總結。由於瀏覽器的同源策略,不同域名、不同端口、不同協議都會構成跨域;但在實際的業務中,很多場景需要進行跨域傳遞信息,這樣就催生出多種跨域方法。
1. 具備src的標簽
-
原理:所有具有 src 屬性的HTML標簽都是可以跨域的
在瀏覽器中, <script> 、 <img> 、 <iframe> 和 <link> 這幾個標簽是可以加載跨域(非同源)的資源的,並且加載的方式其實相當於一次普通的GET請求,唯一不同的是,為了安全起見,瀏覽器不允許這種方式下對加載到的資源的讀寫操作,而只能使用標簽本身應當具備的能力(比如腳本執行、樣式應用等等)。
2. JSONP跨域
-
原理: <script> 是可以跨域的,而且在跨域腳本中可以直接回調當前腳本的函數
script標簽是可以加載異域的JavaScript並執行的,通過預先設定好的callback函數來實現和母頁面的交互。它有一個大名,叫做JSONP跨域,JSONP是JSON with Padding的略稱。它是一個非官方的協議,明明是加載script,為啥和JSON扯上關系呢?原來就是這個callback函數,對它的使用有一個典型的方式,就是通過JSON來傳參,即將JSON數據填充進回調函數,這就是JSONP的JSON+Padding的含義。JSONP只支持GET請求。
前端代碼:
<script type="text/javascript"> function dosomething(jsondata){ //處理獲得的json數據 } </script> <script src="http://haorooms.com/data.php?callback=dosomething"></script>
后台代碼:
<?php $callback = $_GET['callback'];//得到回調函數名 $data = array('a','b','c');//要返回的數據 echo $callback.'('.json_encode($data).')';//輸出 ?>
3. 跨域資源共享(CORS)
-
原理:服務器設置Access-Control-Allow-Origin HTTP響應頭之后,瀏覽器將會允許跨域請求
CORS是HTML5標准提出的跨域資源共享(Cross Origin Resource Share),支持GET、POST等所有HTTP請求。CORS需要服務器端設置 Access-Control-Allow-Origin 頭,否則瀏覽器會因為安全策略攔截返回的信息。
Access-Control-Allow-Origin: * # 允許所有域名訪問,或者 Access-Control-Allow-Origin: http://a.com # 只允許所有域名訪問
CORS又分為簡單跨域和非簡單跨域請求,有關CORS的詳細介紹請看 阮一峰 的 跨域資源共享 CORS 詳解 ,里面講解的非常詳細。
4. document.domain
-
原理:相同主域名不同子域名下的頁面,可以設置document.domain讓它們同域
我們只需要在跨域的兩個頁面中設置document.domain就可以了。修改document.domain的方法只適用於不同子域的框架間的交互,要載入iframe頁面。
例如:1. 在頁面 http://a.example.com/a.html 設置document.domain
<iframe id = "iframe" src="http://b.example.com/b.html" onload = "test()"></iframe> <script type="text/javascript"> document.domain = 'example.com';//設置成主域 function test(){ alert(document.getElementById('iframe').contentWindow);//contentWindow 可取得子窗口的 window 對象 } </script>
2、在頁面http:// b.example.com/b.html 中設置document.domain
<script type="text/javascript"> document.domain = 'example.com';//在iframe載入這個頁面也設置document.domain,使之與主頁面的document.domain相同 </script>
5. window.name
-
原理:window對象有個name屬性,該屬性有個特征:即在一個窗口(window)的生命周期內,窗口載入的所有的頁面都是共享一個window.name的,每個頁面對window.name都有讀寫的權限,window.name是持久存在一個窗口載入過的所有頁面中的。
這里有三個頁面:
-
sever.com/a.html 數據存放頁面
-
agent.com/b.html 數據獲取頁面
-
agent.com/c.html 空頁面,做代理使用
a.html中,設定 window.name 作為需要傳遞的值
<script> window.name = 'I was there!'; alert(window.name); </script>
b.html中,當iframe加載后將iframe的 src 指向同域的 c.html ,這樣就可以利用 iframe.contentWindow.name 獲取要傳遞的值了
<body> <script type="text/javascript"> iframe = document.createElement('iframe'); iframe.style.display = 'none'; var state = 0; iframe.onload = function() { if(state === 1) { var data = JSON.parse(iframe.contentWindow.name); alert(data); iframe.contentWindow.document.write(''); iframe.contentWindow.close(); document.body.removeChild(iframe); } else if(state === 0) { state = 1; iframe.contentWindow.location = 'http://agent.com/c.html'; } }; iframe.src = 'http://sever.com/a.html'; document.body.appendChild(iframe); </script> </body>
成功獲取跨域數據,效果如下:
6. window.postMesage
-
原理: HTML5新增的postMessage方法,通過postMessage來傳遞信息,對方可以通過監聽message事件來監聽信息。可跨主域名及雙向跨域。
這里有兩個頁面:
-
agent.com/index.html
-
server.com/remote.html
本地代碼index.html
<body> <iframe id="proxy" src="http://server.com/remote.html" onload = "postMsg()" style="display: none" ></iframe> <script type="text/javascript"> var obj = { msg: 'hello world' } function postMsg (){ var iframe = document.getElementById('proxy'); var win = iframe.contentWindow; win.postMessage(obj,'http://server.com'); } </script> </body>
postMessage 的使用方法: otherWindow.postMessage(message, targetOrigin);
-
otherWindow: 指目標窗口,也就是給哪個window發消息,是 window.frames 屬性的成員或者由 window.open 方法創建的窗口
-
message: 是要發送的消息,類型為 String、Object (IE8、9 不支持)
-
targetOrigin: 是限定消息接收范圍,不限制請使用 ‘*’
server.com上remote.html,監聽 message 事件,並檢查來源是否是要通信的域。
<head> <title></title> <script type="text/javascript"> window.onmessage = function(e){ if(e.origin !== 'http://localhost:8088') return; alert(e.data.msg+" from "+e.origin); } </script> </head>
7. location.hash
原理:
-
這個辦法比較繞,但是可以解決完全跨域情況下的腳步置換問題。原理是利用location.hash來進行傳值。www.a.com下的a.html想和www.b.com下的b.html通信(在a.html中動態創建一個b.html的iframe來發送請求)
-
但是由於“同源策略”的限制他們無法進行交流(b.html無法返回數據),於是就找個中間人:www.a.com下的c.html(注意是www.a.com下的)。
-
b.html將數據傳給c.html(b.html中創建c.html的iframe),由於c.html和a.html同源,於是可通過c.html將返回的數據傳回給a.html,從而達到跨域的效果。
a.html代碼如下:
<script> function startRequest(){ var ifr = document.createElement('iframe'); ifr.style.display = 'none'; ifr.src = 'http://www.b.com/b.html#sayHi'; //傳遞的location.hash document.body.appendChild(ifr); } function checkHash() { try { var data = location.hash ? location.hash.substring(1) : ''; if (console.log) { console.log('Now the data is '+data); } } catch(e) {}; } setInterval(checkHash, 2000); window.onload = startRequest; </script>
b.html代碼如下:
<script> function checkHash(){ var data = ''; //模擬一個簡單的參數處理操作 switch(location.hash){ case '#sayHello': data = 'HelloWorld';break; case '#sayHi': data = 'HiWorld';break; default: break; } data && callBack('#'+data); } function callBack(hash){ // ie、chrome的安全機制無法修改parent.location.hash,所以要利用一個中間的www.a.com域下的代理iframe var proxy = document.createElement('iframe'); proxy.style.display = 'none'; proxy.src = 'http://localhost:8088/proxy.html'+hash; // 注意該文件在"www.a.com"域下 document.body.appendChild(proxy); } window.onload = checkHash; </script>
由於兩個頁面不在同一個域下,IE、Chrome不允許修改parent.location.hash的值,所以要借助於a.com域名下的一個代理iframe,這里有一個a.com下的代理文件c.html。Firefox可以修改。
c.html代碼如下:
<script>parent.parent.location.hash = self.location.hash.substring(1); </script>
直接訪問a.html,a.html向b.html發送的消息為”sayHi”;b.html通過消息判斷返回了”HiWorld”,並通過c.html改變了location.hash的值
8. 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的話,這個方案就不可行了。
小結
總的來說,常見的跨域方法如上述。在不同的業務場景下,各有適合的跨域方式。跨域解決了一些資源共享、信息交互的難題,但是有的跨域方式可能會帶來安全問題,如jsonp可導致水坑攻擊,等標簽會被用來進行xss或csrf攻擊。所以,在應用跨域的場景,需要格外注意安全問題。