什么是同源策略
瀏覽器出于安全方面的考慮卒暂,只允許與本域下的接口交互抡谐。不同源的客戶端腳本在沒(méi)有明確授權(quán)的情況下纯趋,不能讀寫(xiě)對(duì)方的資源嗅辣。
“同源”指的是”三個(gè)相同“撼泛。
- 協(xié)議相同
- 域名相同
- 端口相同
同源政策的目的,是為了保證用戶信息的安全澡谭,防止惡意的網(wǎng)站竊取數(shù)據(jù)愿题。
隨著互聯(lián)網(wǎng)的發(fā)展,“同源政策”越來(lái)越嚴(yán)格。目前潘酗,如果非同源杆兵,共有三種行為受到限制。
- Cookie仔夺、LocalStorage 和 IndexedDB 無(wú)法讀取琐脏。
- DOM 無(wú)法獲得。
- AJAX 請(qǐng)求無(wú)效(可以發(fā)送缸兔,但瀏覽器會(huì)拒絕接受響應(yīng))日裙。
雖然這些限制是必要的,但是有時(shí)很不方便灶体,合理的用途也受到影響阅签。下面,將詳細(xì)介紹蝎抽,如何規(guī)避上面三種限制政钟。
什么是跨域?跨域有幾種實(shí)現(xiàn)形式
跨域:允許不同域的接口進(jìn)行交互
實(shí)現(xiàn)方式:
- JSONP
- CORS
- 降域
- postMessage
JSONP
JSONP是一種數(shù)據(jù)調(diào)用方式樟结,是解決JSON跨域獲取的解決方案养交。因此JSONP其實(shí)是一個(gè)非官方的協(xié)議。
JSONP是服務(wù)器與客戶端跨源通信的常用方法瓢宦。最大特點(diǎn)就是簡(jiǎn)單適用碎连,老式瀏覽器全部支持,服務(wù)器改造非常小驮履。
JSONP 的主要缺點(diǎn)有兩個(gè)鱼辙,
是只能 GET 不能 POST,因?yàn)槭峭ㄟ^(guò)<script>引用的資源玫镐,參數(shù)全都顯式的放在URL里倒戏,和 AJAX 沒(méi)有關(guān)系。
是存在安全隱患恐似,動(dòng)態(tài)插入<script>標(biāo)簽其實(shí)就是一種腳本注入杜跷,可能遭遇XSS
JSONP原理
網(wǎng)頁(yè)通過(guò)添加一個(gè)<script>元素,向服務(wù)器請(qǐng)求JSON數(shù)據(jù)矫夷,這種做法不受同源政策限制葛闷;服務(wù)器收到請(qǐng)求后,將數(shù)據(jù)放在一個(gè)指定名字的回調(diào)函數(shù)里傳回來(lái)双藕。
JSONP實(shí)現(xiàn)
定義數(shù)據(jù)處理函數(shù)_fun
創(chuàng)建script標(biāo)簽淑趾,src的地址執(zhí)行后端接口,最后加個(gè)參數(shù)callback=_fun
服務(wù)端在收到請(qǐng)求后忧陪,解析參數(shù)扣泊,計(jì)算返還數(shù)據(jù)驳概,輸出 fun(data) 字符串。
fun(data)會(huì)放到script標(biāo)簽做為js執(zhí)行旷赖。此時(shí)會(huì)調(diào)用fun函數(shù),將data做為參數(shù)更卒。
<body>
<div class="container">
<ul class="news">
<li>第11日前瞻:中國(guó)沖擊4金 博爾特再戰(zhàn)</li>
<li>男雙力爭(zhēng)會(huì)師決賽 </li>
<li>女排將死磕巴西等孵!</li>
</ul>
<button class="change">換一組</button>
</div>
<script>
$('.change').addEventListener('click', function(){
var script = document.createElement('script'); // 創(chuàng)建元素節(jié)點(diǎn)
script.src = 'http://localhost:8080/getNews?callback=appendHtml';
// 引入后端接口 后面加上 callback=fun 確定函數(shù)名
document.head.appendChild(script);
// 從后端返回的數(shù)據(jù)作為js代碼執(zhí)行 調(diào)用定義好的函數(shù)
document.head.removeChild(script);
})
function appendHtml(news){
var html = '';
for( var i=0; i<news.length; i++){
html += '<li>' + news[i] + '</li>';
}
$('.news').innerHTML = html;
}
function $(id){
return document.querySelector(id);
}
</script>
</body>
app.get('/getNews', function(req, res){
var news = [
"第11日前瞻:中國(guó)沖擊4金 博爾特再戰(zhàn)200米羽球",
"正直播柴飚/洪煒出戰(zhàn) 男雙力爭(zhēng)會(huì)師決賽",
"女排將死磕巴西!郎平安排男陪練模仿對(duì)方核心",
"沒(méi)有中國(guó)選手和巨星的110米欄 我們還看嗎蹂空?",
"中英上演奧運(yùn)金牌大戰(zhàn)",
]
var data = [];
for(var i=0; i<3; i++){
var index = parseInt(Math.random()*news.length);
data.push(news[index]);
news.splice(index, 1);
}
var cb = req.query.callback;
if(cb){
res.send(cb + '('+ JSON.stringify(data) + ')');
// JSON.stringify(arr) = "["aa","bb","cc"]"
// '('+ JSON.stringify(data) + ')' = '([data1,data2,data3....])'
}else{ res.send(data); }
})
CORS
CORS是跨源資源分享(Cross-Origin Resource Sharing)的縮寫(xiě)俯萌。它是W3C標(biāo)準(zhǔn),是跨源AJAX請(qǐng)求的根本解決方法上枕。
相比JSONP只能發(fā)GET請(qǐng)求咐熙,CORS允許任何類型的請(qǐng)求。
CORS需要瀏覽器和服務(wù)器同時(shí)支持辨萍。目前棋恼,所有瀏覽器都支持該功能,IE瀏覽器不能低于IE10锈玉。
整個(gè)CORS通信過(guò)程爪飘,都是瀏覽器自動(dòng)完成,不需要用戶參與拉背。對(duì)于開(kāi)發(fā)者來(lái)說(shuō)师崎,CORS通信與同源的AJAX通信沒(méi)有差別,代碼完全一樣椅棺。瀏覽器一旦發(fā)現(xiàn)AJAX請(qǐng)求跨源犁罩,就會(huì)自動(dòng)添加一些附加的頭信息,有時(shí)還會(huì)多出一次附加的請(qǐng)求两疚,但用戶不會(huì)有感覺(jué)床估。
因此,實(shí)現(xiàn)CORS通信的關(guān)鍵是服務(wù)器鬼雀。只要服務(wù)器實(shí)現(xiàn)了CORS接口顷窒,就可以跨源通信。
CORS實(shí)現(xiàn)
當(dāng)你使用 XMLHttpRequest發(fā)送請(qǐng)求時(shí)源哩,瀏覽器發(fā)現(xiàn)該請(qǐng)求不符合同源策略鞋吉,會(huì)給該請(qǐng)求加一個(gè)請(qǐng)求頭:Origin,后臺(tái)進(jìn)行一系列處理
如果確定接受請(qǐng)求則在返回結(jié)果中加入一個(gè)響應(yīng)頭:Access-Control-Allow-Origin;
瀏覽器判斷該相應(yīng)頭中是否包含 Origin 的值
如果有則瀏覽器會(huì)處理響應(yīng)励烦,我們就可以拿到響應(yīng)數(shù)據(jù)谓着,
如果不包含瀏覽器直接駁回,這時(shí)我們無(wú)法拿到響應(yīng)數(shù)據(jù)坛掠。
所以 CORS 的表象是讓你覺(jué)得它與同源的 ajax 請(qǐng)求沒(méi)啥區(qū)別赊锚,代碼完全一樣治筒。
瀏覽器將CORS請(qǐng)求分成兩類:簡(jiǎn)單請(qǐng)求(simple request)和非簡(jiǎn)單請(qǐng)求(not-so-simple request)。
只要同時(shí)滿足以下兩大條件舷蒲,就屬于簡(jiǎn)單請(qǐng)求耸袜。
請(qǐng)求方法是以下三種方法之一。
HEAD
GET
POST
HTTP的頭信息不超出以下幾種字段牲平。
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三個(gè)值application/x-www-form-urlencoded堤框、multipart/form-data、text/plain
凡是不同時(shí)滿足上面兩個(gè)條件纵柿,就屬于非簡(jiǎn)單請(qǐng)求蜈抓。
瀏覽器對(duì)這兩種請(qǐng)求的處理,是不一樣的昂儒。
基本流程
對(duì)于簡(jiǎn)單請(qǐng)求沟使,瀏覽器直接發(fā)出CORS請(qǐng)求。具體來(lái)說(shuō)渊跋,就是在頭信息之中腊嗡,增加一個(gè)Origin字段。
下面是一個(gè)例子刹枉,瀏覽器發(fā)現(xiàn)這次跨源AJAX請(qǐng)求是簡(jiǎn)單請(qǐng)求叽唱,就自動(dòng)在頭信息之中,添加一個(gè)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字段用來(lái)說(shuō)明,本次請(qǐng)求來(lái)自哪個(gè)源(協(xié)議 + 域名 + 端口)蟋软。服務(wù)器根據(jù)這個(gè)值镶摘,決定是否同意這次請(qǐng)求。
如果Origin指定的源岳守,不在許可范圍內(nèi)凄敢,服務(wù)器會(huì)返回一個(gè)正常的HTTP回應(yīng)。瀏覽器發(fā)現(xiàn)湿痢,這個(gè)回應(yīng)的頭信息沒(méi)有包含Access-Control-Allow-Origin字段(詳見(jiàn)下文)涝缝,就知道出錯(cuò)了,從而拋出一個(gè)錯(cuò)誤譬重,被XMLHttpRequest的onerror回調(diào)函數(shù)捕獲拒逮。注意,這種錯(cuò)誤無(wú)法通過(guò)狀態(tài)碼識(shí)別臀规,因?yàn)镠TTP回應(yīng)的狀態(tài)碼有可能是200滩援。
如果Origin指定的域名在許可范圍內(nèi),服務(wù)器返回的響應(yīng)塔嬉,會(huì)多出幾個(gè)頭信息字段玩徊。
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
上面的頭信息之中租悄,有三個(gè)與CORS請(qǐng)求相關(guān)的字段,都以Access-Control-開(kāi)頭恩袱。
(1)Access-Control-Allow-Origin
該字段是必須的泣棋。它的值要么是請(qǐng)求時(shí)Origin字段的值,要么是一個(gè)*畔塔,表示接受任意域名的請(qǐng)求外傅。
(2)Access-Control-Allow-Credentials
該字段可選。它的值是一個(gè)布爾值俩檬,表示是否允許發(fā)送Cookie。默認(rèn)情況下碾盟,Cookie不包括在CORS請(qǐng)求之中棚辽。設(shè)為true,即表示服務(wù)器明確許可冰肴,Cookie可以包含在請(qǐng)求中屈藐,一起發(fā)給服務(wù)器。這個(gè)值也只能設(shè)為true熙尉,如果服務(wù)器不要瀏覽器發(fā)送Cookie联逻,刪除該字段即可。
(3)Access-Control-Expose-Headers
該字段可選检痰。CORS請(qǐng)求時(shí)包归,XMLHttpRequest對(duì)象的getResponseHeader()方法只能拿到6個(gè)基本字段:Cache-Control、Content-Language铅歼、Content-Type公壤、Expires、Last-Modified椎椰、Pragma厦幅。如果想拿到其他字段,就必須在Access-Control-Expose-Headers里面指定慨飘。上面的例子指定确憨,getResponseHeader('FooBar')可以返回FooBar字段的值。
withCredentials 屬性
上面說(shuō)到瓤的,CORS請(qǐng)求默認(rèn)不包含Cookie信息(以及HTTP認(rèn)證信息等)休弃。如果需要包含Cookie信息,一方面要服務(wù)器同意堤瘤,指定Access-Control-Allow-Credentials字段玫芦。
Access-Control-Allow-Credentials: true
另一方面,開(kāi)發(fā)者必須在AJAX請(qǐng)求中打開(kāi)withCredentials屬性本辐。
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
否則桥帆,即使服務(wù)器同意發(fā)送Cookie医增,瀏覽器也不會(huì)發(fā)送±铣妫或者叶骨,服務(wù)器要求設(shè)置Cookie,瀏覽器也不會(huì)處理祈匙。
但是忽刽,如果省略withCredentials設(shè)置,有的瀏覽器還是會(huì)一起發(fā)送Cookie夺欲。這時(shí)跪帝,可以顯式關(guān)閉withCredentials。
xhr.withCredentials = false;
需要注意的是些阅,如果要發(fā)送Cookie伞剑,Access-Control-Allow-Origin就不能設(shè)為星號(hào),必須指定明確的市埋、與請(qǐng)求網(wǎng)頁(yè)一致的域名黎泣。同時(shí),Cookie依然遵循同源政策缤谎,只有用服務(wù)器域名設(shè)置的Cookie才會(huì)上傳抒倚,其他域名的Cookie并不會(huì)上傳,且(跨源)原網(wǎng)頁(yè)代碼中的document.cookie也無(wú)法讀取服務(wù)器域名下的Cookie坷澡。
簡(jiǎn)單例子:
瀏覽器判斷該相應(yīng)頭中是否包含 Origin 的值
如果不包含瀏覽器直接駁回托呕,這時(shí)我們無(wú)法拿到響應(yīng)數(shù)據(jù)。
//前端
$('.change').addEventListener('click', function () {
var xhr = new XMLHttpRequest();
xhr.open('get', 'http://b.jrg.com:8080/getNews', true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
appendHtml(JSON.parse(xhr.responseText))
}
};
window.xhr = xhr
});
結(jié)果會(huì)發(fā)生報(bào)錯(cuò):沒(méi)有響應(yīng)頭被允許
如果有則瀏覽器會(huì)處理響應(yīng)频敛,我們就可以拿到響應(yīng)數(shù)據(jù)镣陕,
//后端
res.header("Access-Control-Allow-Origin", "http://a.jrg.com:8080");
//res.header("Access-Control-Allow-Origin", "*");
可實(shí)現(xiàn)CORS跨域:響應(yīng)頭被允許
降域
Cookie 是服務(wù)器寫(xiě)入瀏覽器的一小段信息,只有同源的網(wǎng)頁(yè)才能共享姻政。但是呆抑,兩個(gè)網(wǎng)頁(yè)一級(jí)域名相同,只是二級(jí)域名不同汁展,瀏覽器允許通過(guò)設(shè)置document.domain共享 Cookie鹊碍。(這種方法又被稱為 降域)
舉例來(lái)說(shuō),A網(wǎng)頁(yè)是http://w1.example.com/a.html食绿,B網(wǎng)頁(yè)是http://w2.example.com/b.html侈咕,那么只要設(shè)置相同的document.domain,兩個(gè)網(wǎng)頁(yè)就可以共享Cookie器紧。
注意耀销,這種方法只適用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexedDB 無(wú)法通過(guò)這種方法铲汪,規(guī)避同源政策熊尉,而要使用下文介紹的PostMessage API罐柳。
另外,服務(wù)器也可以在設(shè)置 Cookie 的時(shí)候狰住,指定 Cookie 的所屬域名為一級(jí)域名张吉,比如.example.com。
Set-Cookie: key=value; domain=.example.com; path=/
這樣的話催植,二級(jí)域名和三級(jí)域名不用做任何設(shè)置肮蛹,都可以讀取這個(gè)Cookie。
iframe元素可以在當(dāng)前網(wǎng)頁(yè)之中创南,嵌入其他網(wǎng)頁(yè)伦忠。每個(gè)iframe元素形成自己的窗口,即有自己的window對(duì)象稿辙。iframe窗口之中的腳本缓苛,可以獲得父窗口和子窗口。但是邓深,只有在同源的情況下,父窗口和子窗口才能通信笔刹;如果跨域芥备,就無(wú)法拿到對(duì)方的DOM。
比如舌菜,父窗口運(yùn)行下面的命令萌壳,如果iframe窗口不是同源,就會(huì)報(bào)錯(cuò)日月。
document.getElementById("myIFrame").contentWindow.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.
上面命令中袱瓮,父窗口想獲取子窗口的DOM,因?yàn)榭缬驅(qū)е聢?bào)錯(cuò)爱咬。
反之亦然尺借,子窗口獲取主窗口的DOM也會(huì)報(bào)錯(cuò)。
window.parent.document.body
// 報(bào)錯(cuò)
這種情況不僅適用于iframe窗口精拟,還適用于window.open方法打開(kāi)的窗口燎斩,只要跨域,父窗口與子窗口之間就無(wú)法通信蜂绎。
如果兩個(gè)窗口一級(jí)域名相同栅表,只是二級(jí)域名不同,那么設(shè)置上一節(jié)介紹的document.domain屬性师枣,就可以規(guī)避同源政策怪瓶,拿到DOM。
父頁(yè)面 子頁(yè)面的script代碼里都加上
doucument.domain = 'example.com'
對(duì)于完全不同源的網(wǎng)站践美,目前有兩種方法洗贰,可以解決跨域窗口的通信問(wèn)題找岖。
片段識(shí)別符(fragment identifier)
跨文檔通信API(Cross-document messaging)
postmessage
HTML5為了解決跨域問(wèn)題,引入了一個(gè)全新的API:跨文檔通信 API(Cross-document messaging)哆姻。
這個(gè)API為window對(duì)象新增了一個(gè)window.postMessage方法宣增,允許跨窗口通信,不論這兩個(gè)窗口是否同源矛缨。
舉例來(lái)說(shuō)爹脾,父窗口aaa.com向子窗口bbb.com發(fā)消息,調(diào)用postMessage方法就可以了箕昭。
var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://bbb.com');
postMessage方法的第一個(gè)參數(shù)是具體的信息內(nèi)容灵妨,第二個(gè)參數(shù)是接收消息的窗口的源(origin),即“協(xié)議 + 域名 + 端口”落竹。也可以設(shè)為*泌霍,表示不限制域名,向所有窗口發(fā)送述召。
子窗口向父窗口發(fā)送消息的寫(xiě)法類似朱转。
window.opener.postMessage('Nice to see you', 'http://aaa.com');
父窗口和子窗口都可以通過(guò)message事件,監(jiān)聽(tīng)對(duì)方的消息积暖。
window.addEventListener('message', function(e) {
console.log(e.data);
},false);
message事件的事件對(duì)象event藤为,提供以下三個(gè)屬性。
event.source:發(fā)送消息的窗口
event.origin: 消息發(fā)向的網(wǎng)址
event.data: 消息內(nèi)容
下面的例子是夺刑,子窗口通過(guò)event.source屬性引用父窗口缅疟,然后發(fā)送消息。
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
event.source.postMessage('Nice to see you!', '*');
}
上面代碼有幾個(gè)地方需要注意遍愿。首先存淫,receiveMessage函數(shù)里面沒(méi)有過(guò)濾信息的來(lái)源,任意網(wǎng)址發(fā)來(lái)的信息都會(huì)被處理沼填。其次桅咆,postMessage方法中指定的目標(biāo)窗口的網(wǎng)址是一個(gè)星號(hào),表示該信息可以向任意網(wǎng)址發(fā)送坞笙。通常來(lái)說(shuō)轧邪,這兩種做法是不推薦的,因?yàn)椴粔虬踩吆#赡軙?huì)被惡意利用忌愚。
event.origin屬性可以過(guò)濾不是發(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);
}
}
LocalStorage
通過(guò)window.postMessage却邓,讀寫(xiě)其他窗口的 LocalStorage 也成為了可能硕糊。
<div class="ct">
<h1>使用postMessage實(shí)現(xiàn)跨域</h1>
<div class="main">
<input type="text" placeholder="http://a.jrg.com:8080/a.html">
</div>
<iframe src="http://localhost:8080/b.html" frameborder="0" ></iframe>
</div>
<script>
//URL: http://a.jrg.com:8080/a.html
$('.main input').addEventListener('input', function(){
console.log(this.value);
window.frames[0].postMessage(this.value,'*'); // postMessage
})
window.addEventListener('message',function(e) {
$('.main input').value = e.data
console.log(e.data);
});
function $(id){ return document.querySelector(id); }
</script>
<input id="input" type="text" placeholder="http://b.jrg.com:8080/b.html">
<script>
// URL: http://b.jrg.com:8080/b.html
$('#input').addEventListener('input', function(){
window.parent.postMessage(this.value, '*'); // postMessage
})
window.addEventListener('message',function(e) {
$('#input').value = e.data
console.log(e.data);
});
function $(id){ return document.querySelector(id); }
</script>