跨域是什么
同源策略
在講解什么是跨域之前先要清楚什么是同源策略壳繁,“同源政策”(same-origin policy)是瀏覽器安全的基石盗棵。下面將講解同源策略的相關(guān)概念冰更。
1.1 含義
1995年贰健,同源政策由 Netscape 公司引入瀏覽器北滥。目前刚操,所有瀏覽器都實行這個政策。
最初再芋,它的含義是指菊霜,A網(wǎng)頁設(shè)置的 Cookie,B網(wǎng)頁不能打開济赎,除非這兩個網(wǎng)頁“同源”鉴逞。所謂“同源”指的是”三個相同“,即:
協(xié)議相同
域名相同
端口相同
1.2 目的
同源政策的目的司训,是為了保證用戶信息的安全构捡,防止惡意的網(wǎng)站竊取數(shù)據(jù)。
設(shè)想這樣一種情況: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ā)展捅厂,“同源政策”越來越嚴(yán)格贯卦。目前,如果非同源(也稱非本域)焙贷,共有三種行為受到限制:
(1) Cookie撵割、LocalStorage 和 IndexedDB 無法讀取。
(2) DOM 無法獲得辙芍。
(3) AJAX 請求不能發(fā)送啡彬。
雖然這些限制是必要的,但是有時很不方便故硅,合理的用途也受到影響庶灿。下面,我將詳細介紹吃衅,如何規(guī)避上面三種限制往踢。
如何實現(xiàn)跨域
1. JSONP實現(xiàn)跨域
JSONP是服務(wù)器與客戶端跨源通信的常用方法。最大特點就是簡單適用徘层,老式瀏覽器全部支持峻呕,服務(wù)器改造非常小。
它的基本思想是趣效,網(wǎng)頁通過添加一個<script>元素瘦癌,向服務(wù)器請求JSON數(shù)據(jù),這種做法不受同源政策限制英支;服務(wù)器收到請求后佩憾,將數(shù)據(jù)放在一個指定名字的回調(diào)函數(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>元素池凄,向服務(wù)器localhost:8080發(fā)出請求抡驼。注意,該請求的查詢字符串有一個callback參數(shù)肿仑,用來指定回調(diào)函數(shù)的名字致盟,這對于JSONP是必需的。
讓我們看看后臺是怎么處理請求的:
var cb = req.query.callback;
if(cb){
res.send(cb + '('+ JSON.stringify(data) + ')');
}else{
res.send(data);
}
服務(wù)器收到這個請求以后尤慰,如果發(fā)現(xiàn)請求的query中有先前約定好的callback參數(shù)馏锡,就會將后臺數(shù)據(jù)放在回調(diào)函數(shù)callback的參數(shù)位置返回,否則直接返回后臺數(shù)據(jù)伟端。注意返回的是字符串杯道。
在前端的HTML里我們已經(jīng)定義了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標(biāo)準(zhǔn),是跨源AJAX請求的根本解決方法齿拂。相比JSONP只能發(fā)GET請求驳规,CORS允許任何類型的請求。
2.1 簡介
CORS需要瀏覽器和服務(wù)器同時支持署海。目前吗购,所有瀏覽器都支持該功能,IE瀏覽器不能低于IE10叹侄。
整個CORS通信過程巩搏,都是瀏覽器自動完成,不需要用戶參與趾代。對于開發(fā)者來說,CORS通信與同源的AJAX通信沒有差別丰辣,代碼完全一樣撒强。瀏覽器一旦發(fā)現(xiàn)AJAX請求跨源,就會自動添加一些附加的頭信息笙什,有時還會多出一次附加的請求飘哨,但用戶不會有感覺羡滑。因此娩井,實現(xiàn)CORS通信的關(guān)鍵是服務(wù)器。只要服務(wù)器實現(xiàn)了CORS接口崖瞭,就可以跨源通信统屈。
后臺的實現(xiàn):
res.header("Access-Control-Allow-Origin", "*");
res.send(data);
上面的代碼在響應(yīng)頭添加了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é)議 + 域名 + 端口)统抬。服務(wù)器根據(jù)這個值火本,決定是否同意這次請求。
如果Origin指定的源聪建,不在許可范圍內(nèi)钙畔,服務(wù)器會返回一個正常的HTTP回應(yīng)。瀏覽器發(fā)現(xiàn)金麸,這個回應(yīng)的頭信息沒有包含Access-Control-Allow-Origin字段(詳見下文)擎析,就知道出錯了,從而拋出一個錯誤挥下,被XMLHttpRequest的onerror回調(diào)函數(shù)捕獲揍魂。注意,這種錯誤無法通過狀態(tài)碼識別棚瘟,因為HTTP回應(yīng)的狀態(tài)碼有可能是200现斋。
如果Origin指定的域名在許可范圍內(nèi),服務(wù)器返回的響應(yīng)偎蘸,會多出幾個頭信息字段:
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請求相關(guān)的字段,都以Access-Control-開頭迷雪。
(1)Access-Control-Allow-Origin
該字段是必須的限书。它的值要么是請求時Origin字段的值,要么是一個*振乏,表示接受任意域名的請求蔗包。
(2)Access-Control-Allow-Credentials
該字段可選。它的值是一個布爾值慧邮,表示是否允許發(fā)送Cookie调限。默認(rèn)情況下,Cookie不包括在CORS請求之中误澳。設(shè)為true耻矮,即表示服務(wù)器明確許可,Cookie可以包含在請求中忆谓,一起發(fā)給服務(wù)器裆装。這個值也只能設(shè)為true,如果服務(wù)器不要瀏覽器發(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標(biāo)準(zhǔn)參考教程中查看
3. iframe
iframe元素可以在當(dāng)前網(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监署,因為跨域?qū)е聢箦e颤专。
反之亦然,子窗口獲取主窗口的DOM也會報錯钠乏。
window.parent.document.body
// 報錯
這種情況不僅適用于iframe窗口栖秕,還適用于window.open方法打開的窗口,只要跨域缓熟,父窗口與子窗口之間就無法通信累魔。
3.1 降域
如果兩個窗口一級域名相同,只是二級域名不同够滑,那么設(shè)置document.domain屬性垦写,就可以規(guī)避同源策略,拿到DOM彰触,把這種方法也叫做降域梯投。
舉例來說,A網(wǎng)頁是//a.jirengu.com/a.html况毅,B網(wǎng)頁是//b.jirengu.com/b.html分蓖,那么只要設(shè)置相同的document.domain,兩個網(wǎng)頁就可以相互訪問數(shù)據(jù)尔许。
在兩個網(wǎng)站對應(yīng)的HTML在都要設(shè)置:
document.domain = "jrg.com"
tip:document.domain還可以使兩個一級域名相同么鹤,只是二級域名不同的網(wǎng)站共享 Cookie。
3.2 對于完全不同源的網(wǎng)站味廊,目前有兩種方法蒸甜,可以解決跨域窗口的通信問題:
片段識別符(fragment identifier)
跨文檔通信API(Cross-document messaging)
a)片段標(biāo)識符(fragment identifier)指的是,URL的#號后面的部分余佛,比如http://example.com/x.html#fragment的#fragment柠新。如果只是改變片段標(biāo)識符,頁面不會重新刷新辉巡。
父窗口可以把信息恨憎,寫入子窗口的片段標(biāo)識符:
var src = originURL + '#' + data;
document.getElementById('myIFrame').src = src;
子窗口通過監(jiān)聽hashchange事件得到通知:
window.onhashchange = checkMessage;
function checkMessage() {
var message = window.location.hash;
// ...
}
同樣的,子窗口也可以改變父窗口的片段標(biāo)識符:
parent.location.href= target + “#” + hash;
b)window.postMessage
上面兩種方法都屬于破解郊楣,HTML5為了解決這個問題憔恳,引入了一個全新的API:跨文檔通信 API(Cross-document messaging)。
這個API為window對象新增了一個window.postMessage方法痢甘,允許跨窗口通信喇嘱,不論這兩個窗口是否同源。
舉例來說塞栅,父窗口aaa.com向子窗口bbb.com發(fā)消息者铜,調(diào)用postMessage方法就可以了:
var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://bbb.com');
postMessage方法的第一個參數(shù)是具體的信息內(nèi)容腔丧,第二個參數(shù)是接收消息的窗口的源(origin),即“協(xié)議 + 域名 + 端口”作烟。也可以設(shè)為*愉粤,表示不限制域名,向所有窗口發(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: 消息內(nèi)容
下面的例子是压恒,子窗口通過event.source屬性引用父窗口影暴,然后發(fā)送消息:
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
event.source.postMessage('Nice to see you!', '*');
}
上面代碼有幾個地方需要注意。首先探赫,receiveMessage函數(shù)里面沒有過濾信息的來源型宙,任意網(wǎng)址發(fā)來的信息都會被處理。其次伦吠,postMessage方法中指定的目標(biāo)窗口的網(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);
}
}
小結(jié)
本文介紹了同源策略腺逛,跨域的概念,簡單介紹了JSONP衡怀、CROS以及iframe使用場景下通過降域屉来、片段標(biāo)識符、以及postMessage API來實現(xiàn)跨域的方法狈癞,讓我們對跨域的實現(xiàn)有了較為全面的認(rèn)識。本文的例子來自饑人谷官方視頻教程和阮一峰JS標(biāo)準(zhǔn)參考教程(alpha)