跨域是什么
同源策略
在講解什么是跨域之前先要清楚什么是同源策略,“同源政策”(same-origin policy)是瀏覽器安全的基石。下面將講解同源策略的相關概念。
1.1 含義
1995年,同源政策由 Netscape 公司引入瀏覽器。目前,所有瀏覽器都實行這個政策。
最初,它的含義是指,A網(wǎng)頁設置的 Cookie,B網(wǎng)頁不能打開,除非這兩個網(wǎng)頁“同源”。所謂“同源”指的是”三個相同“,即:
協(xié)議相同
域名相同
端口相同
1.2 目的
同源政策的目的,是為了保證用戶信息的安全,防止惡意的網(wǎng)站竊取數(shù)據(jù)。
設想這樣一種情況:A網(wǎng)站是一家銀行,用戶登錄以后,又去瀏覽其他網(wǎng)站。如果其他網(wǎng)站可以讀取A網(wǎng)站的 Cookie,會發(fā)生什么?
很顯然,如果 Cookie 包含隱私(比如存款總額),這些信息就會泄漏。更可怕的是,Cookie 往往用來保存用戶的登錄狀態(tài),如果用戶沒有退出登錄,其他網(wǎng)站就可以冒充用戶,為所欲為。因為瀏覽器同時還規(guī)定,提交表單不受同源政策的限制。
由此可見,“同源政策”是必需的,否則 Cookie 可以共享,互聯(lián)網(wǎng)就毫無安全可言了。
1.3 限制范圍
隨著互聯(lián)網(wǎng)的發(fā)展,“同源政策”越來越嚴格。目前,如果非同源(也稱非本域),共有三種行為受到限制:
(1) Cookie、LocalStorage 和 IndexedDB 無法讀取。
(2) DOM 無法獲得。
(3) AJAX 請求不能發(fā)送。
雖然這些限制是必要的,但是有時很不方便,合理的用途也受到影響。下面,我將詳細介紹,如何規(guī)避上面三種限制。
如何實現(xiàn)跨域
1. JSONP實現(xiàn)跨域
JSONP是服務器與客戶端跨源通信的常用方法。最大特點就是簡單適用,老式瀏覽器全部支持,服務器改造非常小。
它的基本思想是,網(wǎng)頁通過添加一個<script>元素,向服務器請求JSON數(shù)據(jù),這種做法不受同源政策限制;服務器收到請求后,將數(shù)據(jù)放在一個指定名字的回調函數(shù)里傳回來。
首先,網(wǎng)頁動態(tài)插入<script>元素,由它向跨源網(wǎng)址發(fā)出請求。
$('.change').addEventListener('click', function(){
var script = document.createElement('script');
script.src = 'http://localhost:8080/getNews?callback=appendHtml';
document.head.appendChild(script);
document.head.removeChild(script);
})
上面代碼通過動態(tài)添加<script>元素,向服務器localhost:8080發(fā)出請求。注意,該請求的查詢字符串有一個callback參數(shù),用來指定回調函數(shù)的名字,這對于JSONP是必需的。
讓我們看看后臺是怎么處理請求的:
var cb = req.query.callback;
if(cb){
res.send(cb + '('+ JSON.stringify(data) + ')');
}else{
res.send(data);
}
服務器收到這個請求以后,如果發(fā)現(xiàn)請求的query中有先前約定好的callback參數(shù),就會將后臺數(shù)據(jù)放在回調函數(shù)callback的參數(shù)位置返回,否則直接返回后臺數(shù)據(jù)。注意返回的是字符串。
在前端的HTML里我們已經定義了callback()函數(shù)如下:(callabck=appendHtml)
function appendHtml(news){
var html = '';
for( var i=0; i<news.length; i++){
html += '<li>' + news[i] + '</li>';
}
console.log(html);
$('.news').innerHTML = html;
}
如此一來,后臺的數(shù)據(jù)就會展現(xiàn)在前端頁面,實現(xiàn)了跨域訪問數(shù)據(jù)。
2. CORS實現(xiàn)跨域
CORS是跨源資源分享(Cross-Origin Resource Sharing)的縮寫。它是W3C標準,是跨源AJAX請求的根本解決方法。相比JSONP只能發(fā)GET請求,CORS允許任何類型的請求。
2.1 簡介
CORS需要瀏覽器和服務器同時支持。目前,所有瀏覽器都支持該功能,IE瀏覽器不能低于IE10。
整個CORS通信過程,都是瀏覽器自動完成,不需要用戶參與。對于開發(fā)者來說,CORS通信與同源的AJAX通信沒有差別,代碼完全一樣。瀏覽器一旦發(fā)現(xiàn)AJAX請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感覺。因此,實現(xiàn)CORS通信的關鍵是服務器。只要服務器實現(xiàn)了CORS接口,就可以跨源通信。
后臺的實現(xiàn):
res.header("Access-Control-Allow-Origin", "*");
res.send(data);
上面的代碼在響應頭添加了Access-Control-Allow-Origin:*,讓所有網(wǎng)站可以訪問該網(wǎng)站后臺的數(shù)據(jù)。
2.2 兩種請求
瀏覽器將CORS請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。
只要同時滿足以下兩大條件,就屬于簡單請求。
(1)請求方法是以下三種方法之一:
HEAD
GET
POST
(2)HTTP的頭信息不超出以下幾種字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三個值application/x-www-form-urlencoded、multipart/form-data、text/plain
凡是不同時滿足上面兩個條件,就屬于非簡單請求。
瀏覽器對這兩種請求的處理,是不一樣的。
2.3 簡單請求的處理
對于簡單請求,瀏覽器直接發(fā)出CORS請求。具體來說,就是在頭信息之中,增加一個Origin字段。
下面是一個例子,瀏覽器發(fā)現(xiàn)這次跨源AJAX請求是簡單請求,就自動在頭信息之中,添加一個Origin字段:
GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
上面的頭信息中,Origin字段用來說明,本次請求來自哪個源(協(xié)議 + 域名 + 端口)。服務器根據(jù)這個值,決定是否同意這次請求。
如果Origin指定的源,不在許可范圍內,服務器會返回一個正常的HTTP回應。瀏覽器發(fā)現(xiàn),這個回應的頭信息沒有包含Access-Control-Allow-Origin字段(詳見下文),就知道出錯了,從而拋出一個錯誤,被XMLHttpRequest的onerror回調函數(shù)捕獲。注意,這種錯誤無法通過狀態(tài)碼識別,因為HTTP回應的狀態(tài)碼有可能是200。
如果Origin指定的域名在許可范圍內,服務器返回的響應,會多出幾個頭信息字段:
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
上面的頭信息之中,有三個與CORS請求相關的字段,都以Access-Control-開頭。
(1)Access-Control-Allow-Origin
該字段是必須的。它的值要么是請求時Origin字段的值,要么是一個*,表示接受任意域名的請求。
(2)Access-Control-Allow-Credentials
該字段可選。它的值是一個布爾值,表示是否允許發(fā)送Cookie。默認情況下,Cookie不包括在CORS請求之中。設為true,即表示服務器明確許可,Cookie可以包含在請求中,一起發(fā)給服務器。這個值也只能設為true,如果服務器不要瀏覽器發(fā)送Cookie,刪除該字段即可。
(3)Access-Control-Expose-Headers
該字段可選。CORS請求時,XMLHttpRequest對象的getResponseHeader()方法只能拿到6個基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必須在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。
相對JSONP而言,CORS與其使用目的相同,但是比JSONP更強大。
JSONP只支持GET請求,CORS支持所有類型的HTTP請求。JSONP的優(yōu)勢在于支持老式瀏覽器,以及可以向不支持CORS的網(wǎng)站請求數(shù)據(jù)。
tip:對于非簡單請求(比如:請求方法是PUT或DELETE,或者Content-Type字段的類型是application/json。可以在阮一峰JS標準參考教程中查看
3. iframe
iframe元素可以在當前網(wǎng)頁之中,嵌入其他網(wǎng)頁。每個iframe元素形成自己的窗口,即有自己的window對象。iframe窗口之中的腳本,可以獲得父窗口和子窗口。但是,只有在同源的情況下,父窗口和子窗口才能通信;如果跨域,就無法拿到對方的DOM。
比如,父窗口運行下面的命令,如果iframe窗口不是同源,就會報錯。
document.getElementById("myIFrame").contentWindow.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.
上面命令中,父窗口想獲取子窗口的DOM,因為跨域導致報錯。
反之亦然,子窗口獲取主窗口的DOM也會報錯。
window.parent.document.body
// 報錯
這種情況不僅適用于iframe窗口,還適用于window.open方法打開的窗口,只要跨域,父窗口與子窗口之間就無法通信。
3.1 降域
如果兩個窗口一級域名相同,只是二級域名不同,那么設置document.domain屬性,就可以規(guī)避同源策略,拿到DOM,把這種方法也叫做降域。
舉例來說,A網(wǎng)頁是//a.jirengu.com/a.html,B網(wǎng)頁是//b.jirengu.com/b.html,那么只要設置相同的document.domain,兩個網(wǎng)頁就可以相互訪問數(shù)據(jù)。
在兩個網(wǎng)站對應的HTML在都要設置:
document.domain = "jrg.com"
tip:document.domain還可以使兩個一級域名相同,只是二級域名不同的網(wǎng)站共享 Cookie。
3.2 對于完全不同源的網(wǎng)站,目前有兩種方法,可以解決跨域窗口的通信問題:
片段識別符(fragment identifier)
跨文檔通信API(Cross-document messaging)
a)片段標識符(fragment identifier)指的是,URL的#號后面的部分,比如http://example.com/x.html#fragment的#fragment。如果只是改變片段標識符,頁面不會重新刷新。
父窗口可以把信息,寫入子窗口的片段標識符:
var src = originURL + '#' + data;
document.getElementById('myIFrame').src = src;
子窗口通過監(jiān)聽hashchange事件得到通知:
window.onhashchange = checkMessage;
function checkMessage() {
var message = window.location.hash;
// ...
}
同樣的,子窗口也可以改變父窗口的片段標識符:
parent.location.href= target + “#” + hash;
b)window.postMessage
上面兩種方法都屬于破解,HTML5為了解決這個問題,引入了一個全新的API:跨文檔通信 API(Cross-document messaging)。
這個API為window對象新增了一個window.postMessage方法,允許跨窗口通信,不論這兩個窗口是否同源。
舉例來說,父窗口aaa.com向子窗口bbb.com發(fā)消息,調用postMessage方法就可以了:
var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://bbb.com');
postMessage方法的第一個參數(shù)是具體的信息內容,第二個參數(shù)是接收消息的窗口的源(origin),即“協(xié)議 + 域名 + 端口”。也可以設為*,表示不限制域名,向所有窗口發(fā)送。
子窗口向父窗口發(fā)送消息的寫法類似:
window.opener.postMessage('Nice to see you', 'http://aaa.com');
父窗口和子窗口都可以通過message事件,監(jiān)聽對方的消息:
window.addEventListener('message', function(e) {
console.log(e.data);
},false);
message事件的事件對象event,提供以下三個屬性:
event.source:發(fā)送消息的窗口
event.origin: 消息發(fā)向的網(wǎng)址
event.data: 消息內容
下面的例子是,子窗口通過event.source屬性引用父窗口,然后發(fā)送消息:
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
event.source.postMessage('Nice to see you!', '*');
}
上面代碼有幾個地方需要注意。首先,receiveMessage函數(shù)里面沒有過濾信息的來源,任意網(wǎng)址發(fā)來的信息都會被處理。其次,postMessage方法中指定的目標窗口的網(wǎng)址是一個星號,表示該信息可以向任意網(wǎng)址發(fā)送。通常來說,這兩種做法是不推薦的,因為不夠安全,可能會被惡意利用。
event.origin屬性可以過濾不是發(fā)給本窗口的消息:
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
if (event.origin !== 'http://aaa.com') return;
if (event.data === 'Hello World') {
event.source.postMessage('Hello', event.origin);
} else {
console.log(event.data);
}
}
小結
本文介紹了同源策略,跨域的概念,簡單介紹了JSONP、CROS以及iframe使用場景下通過降域、片段標識符、以及postMessage API來實現(xiàn)跨域的方法,讓我們對跨域的實現(xiàn)有了較為全面的認識。本文的例子來自饑人谷官方視頻教程和阮一峰JS標準參考教程(alpha)