JavaScript 跨域彙總

Hhy_9288發表於2018-03-10

什麼是跨域?

在瞭解跨域之前,首先要知道什麼是同源策略(same-origin policy)。簡單來講同源策略就是瀏覽器為了保證使用者資訊的安全,防止惡意的網站竊取資料,禁止不同域之間的JS進行互動。對於瀏覽器而言只要域名、協議、埠其中一個不同就會引發同源策略,從而限制他們之間如下的互動行為:

  1. Cookie、LocalStorage 和 IndexDB 無法讀取。
  2. DOM 無法獲得。
  3. AJAX 請求不能傳送。

那麼有時候我們又不得不去解決不同域之間的js互動,這時候就要解決瀏覽器同源策略的問題,也就是需要跨域。

跨域的解決辦法

一、JSONP

在js中,我們直接用XMLHttpRequest請求不同域上的資料時,是不可以的。但是,在頁面上引入不同域上的js指令碼檔案卻是可以的,script標籤裡的src屬性來完成的,jsonp正是利用這個特性來實現的。 
比如,在桌面新建一個crossDomain.html頁面,它裡面的程式碼需要利用ajax獲取一個不同域上的json資料,假設這個json資料地址是http://192.168.x.xxx/JSONP/jsonpTest.php那麼crossDomain.html中的程式碼就可以這樣:

<script type="text/javascript">
var text = document.querySelector('.text');
function dosomething(jsondata) {
    var str = "";
    for (var i = 0; i < jsondata.length; i++) {
        str += jsondata[i];
    }
    text.innerHTML = '我是JS通過JSONP跨域請求來的資料:'+'<span class="show">'+str+'</span>';
}
</script>
<script type="text/javascript" src="http://192.168.x.xxx/JSONP/jsonpTest.php?callback=dosomething"></script>

可以看到在獲取資料的地址後面還有一個callback引數,按慣例是用這個引數名,但是你用其他的也一樣。當然如果獲取資料的jsonp地址頁面不是你自己能控制的,就得按照提供資料的那一方的規定格式來操作了。

因為是當做一個js檔案來引入的,所以http://192.168.x.xxx/JSONP/jsonpTest.php返回的必須是一個能執行的js檔案,所以這個頁面的php程式碼可能是這樣的:

<?php
    $callback = $_GET['callback'];//得到回掉函式名
    $data = array('a','b','c'); //要返回的資料
    echo $callback.'('.json_encode($data).')'; //輸出
?>

然後在crossDomain.html中列印出返回的jsondata如下:

["a", "b", "c"]

可以看到請求成功了,然後就可以在crossDomain.html這個頁面裡處理這個資料了。 
這樣jsonp的原理就很清楚了,通過script標籤引入一個js檔案,這個js檔案載入成功後會執行我們在url引數中指定的函式,並且會把我們需要的json資料作為引數傳入。所以jsonp是需要伺服器端的頁面進行相應的配合的。 
當然可以直接用一些已經封裝過的庫,這樣就不用每次去建立script標籤了。如下為JQ的跨域API:

    $.getJSON('http://192.168.x.xxx/JSONP/jsonpTest.php?callback=?',function(jsondata){
        console.log(jsondata);//["a", "b", "c"]
        var str = "";
        $.each(jsondata,function(i,index){
            return str += index;
        });
        $(".text1").html('我是JQ通過JSONP跨域請求來的資料:'+'<span class="show">'+str+'</span>');
    });

jquery的getJSON方法會自動生成一個全域性函式來替換callback=?中的問號,之後獲取到資料後又會自動銷燬,實際上就是起一個臨時代理函式的作用。$.getJSON方法會自動判斷是否跨域,不跨域的話,就呼叫普通的ajax方法;跨域的話,則會以非同步載入js檔案的形式來呼叫jsonp的回撥函式。

這裡寫圖片描述

二、通過修改document.domain來跨子域

上面的jsonp是來解決ajax跨域請求的,那麼如果是需要處理 Cookie 和 iframe 該怎麼辦呢?這時候就可以通過修改document.domain來跨子域。兩個網頁一級域名相同,只是二級域名不同,瀏覽器允許通過設定document.domain共享 Cookie或者處理iframe。比如A網頁是http://w1.example.com/a.html,B網頁是http://w2.example.com/b.html,那麼只要設定相同的document.domain,兩個網頁就可以共享Cookie。

document.domain = 'example.com';
//現在,A網頁通過指令碼設定一個 Cookie。
document.cookie = "test1=hello";
//B網頁就可以讀到這個 Cookie。
var allCookie = document.cookie;

注意,這種方法只適用於 Cookie 和 iframe 視窗,LocalStorage 和 IndexDB 無法通過這種方法,規避同源政策,而要使用下文介紹的PostMessage API。 
另外,伺服器也可以在設定Cookie的時候,指定Cookie的所屬域名為一級域名,比如.example.com。

Set-Cookie: key=value; domain=.example.com; path=/
//這樣的話,二級域名和三級域名不用做任何設定,都可以讀取這個Cookie。

不同的iframe 之間(父子或同輩),是能夠獲取到彼此的window物件的,但是你卻不能使用獲取到的window物件的屬性和方法(html5中的postMessage方法是一個例外,還有些瀏覽器比如ie6也可以使用top、parent等少數幾個屬性),總之,你可以當做是隻能獲取到一個幾乎無用的window物件。 
首先說明一下同域之間的iframe是可以操作的。比如http://127.0.0.1/JSONP/a.html裡面嵌入一個iframe指向http://127.0.0.1/myPHP/b.html。那麼在a.html裡面是可以操作iframe裡面的DOM的。

<iframe src="http://127.0.0.1/myPHP/b.html" frameborder="1"></iframe>
<body>
<script type="text/javascript">
var iframe = document.querySelector("iframe");
iframe.onload = function(){
    var win = iframe.contentWindow;
    var doc = win.document;
    var ele = doc.querySelector(".text1");
    var text = ele.innerHTML="123456";
}
</script>

如果兩個網頁不同源,就無法拿到對方的DOM。典型的例子是iframe視窗和window.open方法開啟的視窗,它們與父視窗無法通訊。如果兩個視窗一級域名相同,只是二級域名不同,那麼document.domain屬性,就可以規避同源政策,拿到DOM。 
對於完全不同源的網站,目前有三種方法,可以解決跨域視窗的通訊問題。

  1. 片段識別符(fragment identifier)
  2. window.name
  3. 跨文件通訊API(Cross-document messaging)

三、使用片段識別符來進行跨域

片段識別符號(fragment identifier)指的是,URL的#號後面的部分,比如http://example.com/x.html#fragment的#fragment。如果只是改變片段識別符號,頁面不會重新重新整理。 
父視窗可以把資訊,寫入子視窗的片段識別符號。在父視窗寫入:

document.getElementById('frame').onload = function(){
    var src = "http://127.0.0.1/JSONP/b.html" + '#' + "data";
    this.src = src;
}

子視窗通過監聽hashchange事件得到通知。

window.onload = function(){
    console.log("b.html載入完成")
    window.onhashchange = function(){
        var message = window.location.hash;
        console.log(message)//#data
    };  
}

同樣的,子視窗也可以改變父視窗的片段識別符號。

parent.location.href= target + "#" + hash;

四、使用window.name來進行跨域

window物件有個name屬性,該屬性有個特徵:即在一個視窗(window)的生命週期內,視窗載入的所有的頁面都是共享一個window.name的,每個頁面對window.name都有讀寫的許可權,window.name是持久存在一個視窗載入過的所有頁面中的,並不會因新頁面的載入而進行重置。這個屬性的最大特點是,無論是否同源,只要在同一個視窗裡,前一個網頁設定了這個屬性,後一個網頁可以讀取它。 
比如:有一個頁面a.html,它裡面有這樣的程式碼:

window.name = "我是a頁面設定的";
setTimeout(function(){
    window.location = "http://127.0.0.1/JSONP/b.html";
},1000)

b.html頁面的程式碼:

console.log(window.name);

a.html頁面載入後1秒,跳轉到了b.html頁面,結果b頁面列印出了:

我是a頁面設定的

可以看到在b.html頁面上成功獲取到了它的上一個頁面a.html給window.name設定的值。如果在之後所有載入的頁面都沒對window.name進行修改的話,那麼所有這些頁面獲取到的window.name的值都是a.html頁面設定的那個值。當然,如果有需要,其中的任何一個頁面都可以對window.name的值進行修改。注意,window.name的值只能是字串的形式,這個字串的大小最大能允許2M左右甚至更大的一個容量,具體取決於不同的瀏覽器,但一般是夠用了。 
利用window.name可以對同域或者不同域的之間的js進行互動。 
那麼在a.html頁面中,我們怎麼把b.html頁面載入進來呢?顯然我們不能直接在a.html頁面中通過改變window.location來載入b.html頁面,因為我們想要即使a.html頁面不跳轉也能得到b.html裡的資料。答案就是在a.html頁面中使用一個隱藏的iframe來充當一箇中間人角色,由iframe去獲取b.html的資料,然後a.html再去得到iframe獲取到的資料。

五、window.postMessage

上面兩種方法都屬於破解,HTML5為了解決這個問題,引入了一個全新的API:跨文件通訊 API(Cross-document messaging)。 
這個API為window物件新增了一個window.postMessage方法,允許跨視窗通訊,不論這兩個視窗是否同源。目前IE8+、FireFox、Chrome、Opera等瀏覽器都已經支援window.postMessage方法。 
舉例來說,父視窗http://a.com向子視窗http://b.com發訊息,呼叫postMessage方法就可以了。 
a頁面:

<iframe id="frame1" src="http://127.0.0.1/JSONP/b.html" frameborder="1"></iframe>
document.getElementById('frame1').onload = function(){
    var win = document.getElementById('frame1').contentWindow;
    win.postMessage("我是來自a頁面的","http://127.0.0.1/JSONP/b.html")
}

b頁面通過監聽message事件可以接受到來自a頁面的訊息。

window.onmessage = function(e){
    e = e || event;
    console.log(e.data);//我是來自a頁面的
}

子視窗向父視窗傳送訊息的寫法類似。

window.opener.postMessage('我是來自b頁面的', 'http://a.com');
//父視窗和子視窗都可以通過message事件,監聽對方的訊息。

通過window.postMessage,讀寫其他視窗的 LocalStorage 也成為了可能。 
下面是一個例子,主視窗寫入iframe子視窗的localStorage。 
父視窗傳送訊息程式碼

var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
// 存入物件
win.postMessage(JSON.stringify({key: 'storage', method: 'set', data: obj}), 'http://b.com');
// 讀取物件
win.postMessage(JSON.stringify({key: 'storage', method: "get"}), "*");
window.onmessage = function(e) {
  if (e.origin != 'http://a.com') return;
  // "Jack"
  console.log(JSON.parse(e.data).name);
};

子視窗接收訊息的程式碼

window.onmessage = function(e) {
  if (e.origin !== 'http://bbb.com') return;
  var payload = JSON.parse(e.data);
  switch (payload.method) {
    case 'set':
      localStorage.setItem(payload.key, JSON.stringify(payload.data));
      break;
    case 'get':
      var parent = window.parent;
      var data = localStorage.getItem(payload.key);
      parent.postMessage(data, 'http://aaa.com');
      break;
    case 'remove':
      localStorage.removeItem(payload.key);
      break;
  }
};

六、通過WebSocket進行跨域

WebSocket是一種通訊協議,使用ws://(非加密)和wss://(加密)作為協議字首。該協議不實行同源政策,只要伺服器支援,就可以通過它進行跨源通訊。 
下面是一個例子,瀏覽器發出的WebSocket請求的頭資訊(摘自維基百科)。

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

上面程式碼中,有一個欄位是Origin,表示該請求的請求源(origin),即發自哪個域名。 
正是因為有了Origin這個欄位,所以WebSocket才沒有實行同源政策。因為伺服器可以根據這個欄位,判斷是否許可本次通訊。如果該域名在白名單內,伺服器就會做出如下回應。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

七、 CORS

CORS是跨源資源分享(Cross-Origin Resource Sharing)的縮寫。它是W3C標準,是跨源AJAX請求的根本解決方法。相比JSONP只能發GET請求,CORS允許任何型別的請求。CORS需要瀏覽器和伺服器同時支援。目前,所有瀏覽器都支援該功能,IE瀏覽器不能低於IE10。 
整個CORS通訊過程,都是瀏覽器自動完成,不需要使用者參與。對於開發者來說,CORS通訊與同源的AJAX通訊沒有差別,程式碼完全一樣。瀏覽器一旦發現AJAX請求跨源,就會自動新增一些附加的頭資訊,有時還會多出一次附加的請求,但使用者不會有感覺。 
因此,實現CORS通訊的關鍵是伺服器。只要伺服器實現了CORS介面,就可以跨源通訊。由於CORS涉及內容較多,以後會寫一篇 專門介紹CORS的文章。

八、服務端設定代理頁面專門處理前端跨域請求

總結:以上整理了各種常見的跨域解決辦法,在開發過程中我們可以根據不同的場景選擇最佳的解決辦法。處理ajax的跨域可以選擇JSONP、CORS,服務端設定代理、WebSocket。如果主域相同,處理多級子域之間的通訊可以選擇document.domain,處理不同域之間的iframe,子視窗可以選擇window.name、window.postMessage、location.hash來解決。

本文參考:http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html 
https://my.oschina.net/u/3341316/blog/856682 

相關文章