最近的項目中,在前端項目中訪問另一個前端頁面,同時還有數據的交互,在使用iframe中總是提示跨域請求,在解決問題中,查看了很多資料,同時瞭解了一下前端跨域的原因,以及常見的解決方案,進行總結如下,防止今後再次遇到。
一、跨域
我們定義JS跨域是指通過JS在不同的域中進行相互通信或者數據傳輸。這裏的域一般是指協議、域名(或主機地址)、端口,只要有其中一個不同,都會被當作是不同的域。而這個域是通過瀏覽器的同源策略進行限制的。
二、瀏覽器的同源策略
同源策略/SOP(Same Origin Policy)是一種約定,由Netscape公司1995年引入瀏覽器,它是瀏覽器最核心也最基本的安全功能,如果缺少了同源策略,瀏覽器很容易受到XSS、CSFR等攻擊。
同源策略限制了從同一個源加載的文檔或腳本如何與來自另一個源的資源進行交互。這是一個用於隔離潛在惡意文件的重要安全機制。
在比較URL不同時,我們先來查看一下URL的組成部分,一般URL(統一資源定位符)分爲:
scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]
scheme爲具體的協議,包括http、https、ssh、ftp、pop3等,每個協議一般對應不同的參數默認部分。這裏就講解一下http、https相關的問題。
常見的URL組成爲: 協議+域名(ip)+端口+路徑
如果協議,端口和域名對於兩個頁面是相同的,則兩個頁面具有相同的源。
比如,以http://blog.anumbrella.net/example/index.html
這個URL來說,協議是http,域名爲blog.anumbrella.net,端口爲80(默認端口)。
它的同源情況如下:
URL | 結果 | 原因 |
---|---|---|
http://blog.anumbrella.net/example/other.html | 同源 | 只有路徑不同 |
http://blog.anumbrella.net/inner/another.html | 同源 | 只有路徑不同 |
https://blog.anumbrella.net/example/other.html | 不同源 | 不同協議(http和https) |
http://blog.anumbrella.net:81/example/etc.html | 不同源 | 不同端口(80和81) |
http://dev.anumbrella.net/example/other.html | 不同源 | 不同域名(dev和blog) |
限制範圍
-
Cookie、LocalStorage 和 IndexDB 無法讀取
-
DOM 和 JS對象無法獲得
-
AJAX 請求不能發送
目的
同源策略保障了用戶信息的安全,防止了惡意的網站竊取數據。
比如這樣一種情況:A網站是一家銀行,用戶登錄以後,又去瀏覽其他網站。如果其他網站可以讀取A網站的 Cookie,會發生什麼?
很顯然,如果 Cookie 包含隱私(比如存款總額),這些信息就會泄漏。更可怕的是,Cookie 往往用來保存用戶的登錄狀態,如果用戶沒有退出登錄,其他網站就可以冒充用戶,爲所欲爲。因爲瀏覽器同時還規定,提交表單不受同源政策的限制。
由此可見,"同源政策"是必需的,否則 Cookie 可以共享,互聯網就毫無安全可言了。
需求
既然有安全問題,那爲什麼又要跨域呢? 這是因爲有時公司內部有多個不同的子域,比如一個是location.company.com
, 而應用是放在app.company.com
, 這時想從app.company.com
去訪問location.company.com
的資源就屬於跨域。
跨源網絡訪問
同源策略控制了不同源之間的交互,以下的實例是允許跨域資源嵌入的:
- script標籤允許跨域嵌入腳本,稍後介紹的JSONP就是利用這個“漏洞”來實現。
- img標籤、link標籤、@font-face不受跨域影響。
- video和audio嵌入的資源。
- iframe載入的任何資源。(不是iframe之間的通信)
<object>
、<embed>
和<applet>
的插件。- WebSocket不受同源策略的限制。
三、常見的解決方案
1、通過JSONP跨域
JSONP是JSON with Padding(填充式JSON)的簡寫,是應用JSON的一種新方法,只不過是被包含在函數調用中的JSON。
它通過借用script標籤不受同源限制的這個特性,通過動態的給頁面添加一個script標籤,利用事先聲明好的數據處理函數來獲取數據。
在JSONP中包含兩部分:回調函數和數據。其中,回調函數是當響應到來時要放在當前頁面被調用的函數。而數據,就是傳入回調函數中的JSON字符串,也就是回調函數的參數了。
(1)、原生實現
function handleResponse(response) {
console.log(response.data);
}
var script = document.createElement("script");
script.src = "http://example.com/jsonp/getSomething?uid=123&callback=hadleResponse"
document.body.insertBefore(script, document.body.firstChild);
/*handleResponse({"data": "hey"})*/
它的過程是這樣子的:
- 當我們通過新建一個script標籤請求時,後臺會根據相應的參數來生成相應的JSON數據。比如說上面這個鏈接,傳遞了handleResponse給後臺,然後後臺根據這個參數再結合數據生成了handleResponse({“data”: “hey”})。
- 緊接着,這個返回的JSON數據其實就可以被當成一個js腳本,就是對一個函數的調用。
- 由於我們事先已經聲明瞭這麼一個回調函數,於是當資源加載進來的時候,直接就對函數進行調用,於是數據當然就能獲取到了。
至此,跨域通信完成。
(2)、在JQuery中使用JSONP
在JQuery中的AJAX中,已經封裝了JSONP,下面簡單介紹一下如何去使用。
$.ajax({
url: 'http://dev.anumbrella.net/login',
type: 'get',
dataType: 'jsonp', // 請求方式爲jsonp
jsonpCallback: "handleCallback", // 自定義回調函數名
success: function (data) {
console.log(data);
},
error: function (data) {
console.log(data);
}
});
在AJAX中,主要設置dataType類型爲jsonp
。對於jsonp
參數來說,默認值是callback,而jsonpCallback參數的值默認是JQuery自己生成的。如果想自己指定一個回調函數,可像代碼中對jsonpCallback進行設置。上面的代碼中,最終的URL將會是http://dev.anumbrella.net/login?callback=handleCallback
。
JSONP的優缺點
JSONP的優點是:它不像XMLHttpRequest對象實現的Ajax請求那樣受到同源策略的限制;它的兼容性更好,在更加古老的瀏覽器中都可以運行,不需要XMLHttpRequest或ActiveX的支持;並且在請求完畢後可以通過調用callback的方式回傳結果。
JSONP的缺點則是:它只支持GET請求而不支持POST等其它類型的HTTP請求;它只支持跨域HTTP請求這種情況,不能解決不同域的兩個頁面之間如何進行JavaScript調用的問題。
2、通過修改document.domain+iframe來跨子域
此方案僅限主域相同,子域不同的跨域應用場景。
實現原理:兩個頁面都通過js強制設置document.domain爲基礎主域,就實現了同域。
(1)、父窗口:(http://parent.anumbrella.net/a.html)
<iframe id="iframe" src="http://child.anumbrella.net/b.html"></iframe>
<script>
document.domain = 'anumbrella.net';
var user = 'admin';
</script>
(2)、父窗口:(http://child.anumbrella.net/b.html)
<script>
document.domain = 'anumbrella.net';
// 獲取父窗口中變量
console.log('get js data from parent ---> ' + window.parent.user);
</script>
3、location.hash + iframe跨域
實現原理: a欲與b跨域相互通信,通過中間頁c來實現。 三個頁面,不同域之間利用iframe的location.hash傳值,相同域之間直接js訪問來通信。
具體實現:A域:a.html -> B域:b.html -> A域:c.html,a與b不同域只能通過hash值單向通信,b與c也不同域也只能單向通信,但c與a同域,所以c可通過parent.parent訪問a頁面所有對象。
(1)、a.html:(http://www.example1.com/a.html)
<iframe id="iframe" src="http://www.example2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 向b.html傳hash值
setTimeout(function() {
iframe.src = iframe.src + '#user=admin';
}, 1000);
// 開放給同域c.html的回調方法
function onCallback(res) {
console.log('data from c.html ---> ' + res);
}
</script>
(2)、b.html:(http://www.example2.com/b.html)
<iframe id="iframe" src="http://www.example1.com/c.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 監聽a.html傳來的hash值,再傳給c.html
window.onhashchange = function () {
iframe.src = iframe.src + location.hash;
};
</script>
(3)、c.html:(http://www.example1.com/c.html)
<script>
// 監聽b.html傳來的hash值
window.onhashchange = function () {
// 再通過操作同域a.html的js回調,將結果傳回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>
4、window.name + iframe跨域
window對象有個name屬性,該屬性有個特徵:即在一個窗口(window)的生命週期內,窗口載入的所有的頁面都是共享一個window.name的,每個頁面對window.name都有讀寫的權限,window.name是持久存在一個窗口載入過的所有頁面中的,並不會因新頁面的載入而進行重置。
(1)、a.html:(http://www.example1.com/a.html)
var proxy = function(url, callback) {
var state = 0;
var iframe = document.createElement('iframe');
// 加載跨域頁面
iframe.src = url;
// onload事件會觸發2次,第1次加載跨域頁,並留存數據於window.name
iframe.onload = function() {
if (state === 1) {
// 第2次onload(同域proxy頁)成功後,讀取同域window.name中數據
callback(iframe.contentWindow.name);
destoryFrame();
} else if (state === 0) {
// 第1次onload(跨域頁)成功後,切換到同域代理頁面
iframe.contentWindow.location = 'http://www.example1.com/proxy.html';
state = 1;
}
};
document.body.appendChild(iframe);
// 獲取數據以後銷燬這個iframe,釋放內存;這也保證了安全(不被其他域frame js訪問)
function destoryFrame() {
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
};
// 請求跨域b頁面數據
proxy('http://www.example2.com/b.html', function(data){
console.log(data);
});
(2)、proxy.html:(http://www.example1.com/proxy.html)
中間代理頁,這是一個在www.example1.com域名下的空頁面。
(3)、b.html:(http://www.example2.com/b.html)
<script>
window.name = 'This is domain2 data!';
</script>
這種方法的優點是,window.name容量很大,可以放置非常長的字符串;缺點是必須監聽子窗口window.name屬性的變化,影響網頁性能。
5、window.postMessage
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是爲數不多可以跨域操作的window屬性之一,它可用於解決以下方面的問題:
- 頁面和其打開的新窗口的數據傳遞
- 多窗口之間消息傳遞
- 頁面與嵌套的iframe消息傳遞
- 上面三個場景的跨域數據傳遞
postMessage方法的第一個參數是具體的信息內容,第二個參數是接收消息的窗口的源(origin),即"協議 + 域名 + 端口"。也可以設爲*,表示不限制域名,向所有窗口發送。
父窗口和子窗口都可以通過message事件,監聽對方的消息。
父窗口和子窗口都可以通過message事件,監聽對方的消息。
window.addEventListener('message', function(e) {
console.log(e.data);
},false);
(1)、a.html:(http://www.example1.com/a.html)
<iframe id="iframe" src="http://www.example2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
name: 'aym'
};
// 向example1傳送跨域數據
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.example1.com');
};
// 接受example2返回數據
window.addEventListener('message', function(e) {
console.log('data from example2 ---> ' + e.data);
}, false);
</script>
(2)、b.html:(http://www.example2.com/b.html)
<script>
// 接收domain1的數據
window.addEventListener('message', function(e) {
alert('data from example1 ---> ' + e.data);
var data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 處理後再發回domain1
window.parent.postMessage(JSON.stringify(data), 'http://www.example1.com');
}
}, false);
</script>
message事件的事件對象event,提供以下三個屬性。
- event.source:發送消息的窗口
- event.origin: 消息發向的網址
- event.data: 消息內容
6、Nginx代理跨域
(1)、Nginx配置解決iconfont跨域
瀏覽器跨域訪問js、css、img等常規靜態資源被同源策略許可,但iconfont字體文件(eot|otf|ttf|woff|svg)例外,此時可在nginx的靜態資源服務器中加入以下配置。
location / {
add_header Access-Control-Allow-Origin *;
}
(2)、Nginx反向代理接口跨域
跨域原理: 同源策略是瀏覽器的安全策略,不是HTTP協議的一部分。服務器端調用HTTP接口只是使用HTTP協議,不會執行JS腳本,不需要同源策略,也就不存在跨越問題。
實現思路:通過Nginx配置一個代理服務器(域名與example1相同,端口不同)做跳板機,反向代理訪問example2接口,並且可以順便修改cookie中domain信息,方便當前域cookie寫入,實現跨域登錄。
Nginx的配置如下:
#proxy服務器
server {
listen 81;
server_name www.example1.com;
location / {
proxy_pass http://www.example2.com:8080; #反向代理
proxy_cookie_domain www.dexample1.com www.example2.com; #修改cookie裏域名
index index.html index.htm;
# 當用webpack-dev-server等中間件代理接口訪問nignx時,此時無瀏覽器參與,故沒有同源限制,下面的跨域配置可不啓用
add_header Access-Control-Allow-Origin http://www.example1.com; #當前端只跨域不帶cookie時,可爲*
add_header Access-Control-Allow-Credentials true;
}
}
7、 WebSocket協議跨域
WebSocket是一種通信協議,使用ws://(非加密)和wss://(加密)作爲協議前綴。該協議不實行同源政策,只要服務器支持,就可以通過它進行跨源通信。
WebSocket protocol是HTML5一種新的協議。它實現了瀏覽器與服務器全雙工通信,同時允許跨域通訊,是server push技術的一種很好的實現。
原生WebSocket API使用起來不太方便,我們使用Socket.io,它很好地封裝了webSocket接口,提供了更簡單、靈活的接口,也對不支持webSocket的瀏覽器提供了向下兼容。
前端代碼:
<div>input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io('http://www.example2.com:8080');
// 連接成功處理
socket.on('connect', function() {
// 監聽服務端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
});
// 監聽服務端關閉
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
});
document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value);
};
</script>
更多的使用WebSocket知識,也可以查看我原來寫的文檔——RabbitMQ學習(八)——做WebSocket消息代理,集成Spring Boot實現消息實時推送。
8、 使用CORS允許跨源訪問
CORS是跨源資源分享(Cross-Origin Resource Sharing)的縮寫。它是W3C標準,是跨源AJAX請求的根本解決方法。
目前,所有瀏覽器都支持該功能(IE8+:IE8/9需要使用XDomainRequest對象來支持CORS)),CORS也已經成爲主流的跨域解決方案。
在CORS請求中分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。
簡單請求:如果不用帶cookie,只需服務端設置Access-Control-Allow-Origin即可,前端無須設置,若要帶cookie請求:前後端都需要設置。
CORS與JSONP的使用目的相同,但是比JSONP更強大。
關於更多詳細的關於CORS知識點,可以參考阮一峯老師的博客CORS。
這篇文章主要是對好的解決方案文章進行了總結和整理,更多的詳情,可以查看原文作者,在參考鏈接中。