JavaScript 跨域彙總
什麼是跨域?
在瞭解跨域之前,首先要知道什麼是同源策略(same-origin policy)。簡單來講同源策略就是瀏覽器為了保證使用者資訊的安全,防止惡意的網站竊取資料,禁止不同域之間的JS進行互動。對於瀏覽器而言只要域名、協議、埠其中一個不同就會引發同源策略,從而限制他們之間如下的互動行為:
- Cookie、LocalStorage 和 IndexDB 無法讀取。
- DOM 無法獲得。
- 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。
對於完全不同源的網站,目前有三種方法,可以解決跨域視窗的通訊問題。
- 片段識別符(fragment identifier)
- window.name
- 跨文件通訊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
相關文章
- 前端常見跨域方案彙總前端跨域
- JavaScript跨域相關的總結JavaScript跨域
- JavaScript cookie 跨域JavaScriptCookie跨域
- JavaScript—掌握跨域(24)JavaScript跨域
- JavaScript-CORS 跨域JavaScriptCORS跨域
- JavaScript跨域呼叫、JSONPJavaScript跨域JSON
- 跨域總結跨域
- JavaScript字串API彙總JavaScript字串API
- JavaScript之跨域解決方式JavaScript跨域
- 前端跨域方法總結前端跨域
- 跨域問題總結跨域
- 跨域總結(jquery,php)跨域jQueryPHP
- JavaScript陣列API彙總JavaScript陣列API
- JavaScript知識點彙總JavaScript
- JavaScript資料方法彙總JavaScript
- 關於JavaScript跨域的若干想法JavaScript跨域
- 前端跨域問題總結前端跨域
- axios跨域學習總結iOS跨域
- 跨域知識點部分總結跨域
- 跨域方案總結與實現跨域
- 跨域解決方案(總結篇)跨域
- 跨域問題(普通跨域和springsecurity跨域)跨域SpringGse
- 前端解決跨域問題總結前端跨域
- JavaScript 跨域訪問(API介面)實現原理分析JavaScript跨域API
- 【JavaScript】通過封裝自己的JSONP解決瀏覽器的跨域問題(Ajax跨域)JavaScript封裝JSON瀏覽器跨域
- 對前端跨域方案的認知總結前端跨域
- 跨域跨域
- JavaScript作用域相關的總結JavaScript
- JavaScript常見面試題彙總(含答案)JavaScript面試題
- 玩轉javaScript---知識點彙總(3)JavaScript
- 玩轉javascript---知識點彙總(2)JavaScript
- 玩轉javascript---知識點彙總(1)JavaScript
- HTML、CSS、JavaScript能實現的功能彙總!HTMLCSSJavaScript
- 2018年終總結之AI領域開源框架彙總AI框架
- 前端面試總結之:js跨域問題前端面試JS跨域
- web跨域及cookie相關知識總結Web跨域Cookie
- Tomcat跨域Tomcat跨域
- 跨域CORS跨域CORS