一壶笼、同源策略
同源策略是一種約定,是瀏覽器最核心也最基本的安全功能雁刷,主要體現(xiàn)在同源策略會(huì)限制來(lái)自不同源的文檔和腳本對(duì)當(dāng)前源的文檔數(shù)據(jù)的讀取或設(shè)置某些屬性覆劈,是用于隔離潛在惡意文件的重要安全機(jī)制。
二、如何確定一個(gè)源
只要滿足下面三項(xiàng)相同墩崩,則可以確定兩個(gè)頁(yè)面是來(lái)自同一個(gè)源的
協(xié)議氓英、域名、端口
下面給出相對(duì)http://abc.jjp.com/app/page.html
的同源檢測(cè)結(jié)果
url | 結(jié)果 | 原因 |
---|---|---|
http://abc.jjp.com/app/page2.html |
成功 | |
http://abc.jjp.com/source/page2.html |
成功 | |
https://abc.jjp.com/app/dist/page3.html |
失敗 | 協(xié)議不相同 |
http://efg.jjp.com/app/page2.html |
失敗 | 域名不相同 |
http://abc.jjp.com:8080/app/page2.html |
失敗 | 端口不相同 |
三鹦筹、源的繼承
about:blank铝阐,javascript:中的內(nèi)容,繼承了將其載入的文檔的源铐拐,因?yàn)檫@些偽協(xié)議的URL并沒(méi)有明確地包含有關(guān)服務(wù)器的源的信息徘键。當(dāng)調(diào)用window.open()打開一個(gè)about:blank頁(yè)面時(shí),若該頁(yè)面有代碼遍蟋,則會(huì)繼承創(chuàng)建該頁(yè)面的代碼的源吹害。而data:URLs則會(huì)重新得到一個(gè)新的空的安全的上下文,不會(huì)繼承源
注意:在Gecko 6.0之前虚青,如果用戶在地址欄中輸入 data URLs它呀,data URLs 將繼承當(dāng)前瀏覽器窗口中網(wǎng)頁(yè)的安全上下文
四、IE是例外
在同源策略中棒厘,Internet Explorer有兩點(diǎn)不同
-
授信范圍(Trust Zones)
:兩個(gè)相互之間高度互信的域名纵穿,如公司域名 (corporate domains),不遵守同源策略的限制奢人。 -
端口
:未將端口號(hào)加入到同源策略的組成部分之中谓媒,因此http://abc.com:81/index.html
和http://abc.com/index.html
屬于同源并且不受任何限制。
五何乎、修改源
頁(yè)面可以改變本身的源句惯,但是會(huì)有一些限制。腳本可以將 document.domain
設(shè)置為當(dāng)前域或者當(dāng)前域的超級(jí)域支救,該較短的域會(huì)用于后續(xù)源檢查抢野。
假如當(dāng)前頁(yè)面http://abc.jjp.com/index.html
文檔中執(zhí)行如下腳本,將當(dāng)前域設(shè)置當(dāng)前域的超級(jí)域
document.domain = 'jjp.com'
設(shè)置完之后搂妻,該頁(yè)面會(huì)通過(guò)http://jjp.com/page.html
的同源檢查蒙保,同時(shí)abc.jjp.com
不能設(shè)置為efg.jjp.com
,因?yàn)?code>efg.jjp.com不是abc.jjp.com
的超級(jí)域
如果存在端口號(hào)不一致欲主,想通過(guò)document.domain
設(shè)置的方式來(lái)通過(guò)同源檢查的話需要雙方都進(jìn)行設(shè)置。因?yàn)樵O(shè)置document.domain
會(huì)導(dǎo)致端口號(hào)被重寫為null逝嚎。如果jjp.com:8080想要與jjp.com通信扁瓢,把jjp.com:8080
頁(yè)面的document.domain
設(shè)置為jjp.com
時(shí)端口號(hào)會(huì)被重寫為null,而原來(lái)的jjp.com
的端口號(hào)為80补君,則還是不能夠通過(guò)同源檢測(cè)引几,需要雙方同時(shí)設(shè)置document.domain
讓雙方端口號(hào)都為null。
注意:document.domain
能夠讓子域訪問(wèn)其父域,但是需要同時(shí)將子域和父域的document.domain
設(shè)置為相同的值伟桅。這是必要的敞掘,即使是簡(jiǎn)單的將父域設(shè)置為其原來(lái)的值。不這么做的話可能導(dǎo)致權(quán)限錯(cuò)誤
六楣铁、不同源之間的交互
同源策略控制了不同源的交互玖雁,主要有三類交互
-
跨域?qū)?/strong>:通常允許,比如
鏈接
盖腕、重定向
和表單提交(因?yàn)楸韱翁峤徊恍枰答仈?shù)據(jù))
- 跨域資源嵌入:通常允許赫冬,下面會(huì)給出跨域資源嵌入的例子
-
跨域讀:通常不允許,比如在使用
XMLHttpRequest
的時(shí)候會(huì)發(fā)生跨域問(wèn)題溃列,不過(guò)通過(guò)某些方法仍可以進(jìn)行跨域讀
跨域資源嵌入的例子
-
<script src="..."></script>
標(biāo)簽嵌入外域的腳本劲厌,且該腳本的錯(cuò)誤不能在本源中捕獲 -
<link rel= "stylesheet" href="...">
標(biāo)簽嵌入外域的css文件,由于CSS的松散的語(yǔ)法規(guī)則
听隐,CSS的跨域需要一個(gè)設(shè)置正確的Content-Type
消息頭补鼻,不同瀏覽器有不同的限制。 -
<img>
嵌入外域的圖片 -
<video>和<audio>
標(biāo)簽嵌入外域的多媒體資源 -
<object>和<embed>
的插件 -
@font-face
引入的字體雅任,一些瀏覽器允許引入外域字體风范,一些瀏覽器則不允許 -
<iframe>
載入的任何資源,站點(diǎn)可以使用X-Frame-Options
消息頭來(lái)阻止這種形式的跨域交互
七、如何避免跨域訪問(wèn)
- 避免跨域?qū)?/strong>:在發(fā)起寫請(qǐng)求中攜帶一個(gè)隱藏的token,然后服務(wù)器端對(duì)這個(gè)token進(jìn)行驗(yàn)證可很,多用來(lái)防范CSRF攻擊
- 避免跨域讀:要保證返回給客戶端的資源是不可嵌入的哗戈,不可以是上面列出的允許跨域資源嵌入的標(biāo)簽
- 避免跨域資源嵌入:需要確保html文檔中沒(méi)有上面列出的允許跨域資源嵌入的標(biāo)簽
八、跨源文檔API的訪問(wèn)
javascript
的api中因痛,允許文檔間互相引用,如 iframe.contentWindow
,window.parent
猪腕, window.open
和window.opener
,這些api可以拿到其他文檔的對(duì)象的引用钦勘,但是當(dāng)兩個(gè)文檔不同源時(shí)陋葡,對(duì)該對(duì)象(如Window
、Location
)的訪問(wèn)就會(huì)有所限制彻采。如果想要兩個(gè)不同源的窗口進(jìn)一步交流可以使用window.postMessage
。
九、跨源數(shù)據(jù)存儲(chǔ)訪問(wèn)
localStorage
、IndexedDB
等數(shù)據(jù)存儲(chǔ)會(huì)以源進(jìn)行分割虎囚,每個(gè)源擁有自己獨(dú)立的存儲(chǔ)空間,一個(gè)源的js腳本不能對(duì)屬于其他源的數(shù)據(jù)進(jìn)行讀寫操作
cookies
同樣只有同源網(wǎng)頁(yè)才能共享留攒,設(shè)置其domain剪侮、path瓣俯、secure彩匕、HttpOnly屬性可以來(lái)限定其訪問(wèn)性
屬性 | 作用 |
---|---|
domain | 指定cookies對(duì)哪個(gè)域有效掸犬,cookies只會(huì)發(fā)向該域湾碎,默認(rèn)值是設(shè)置cookie的那個(gè)域 |
path | 表示相對(duì)于domain的路徑递惋,只有在該路徑下才能拿到cookies,默認(rèn)值為/ |
secure | 設(shè)置了該屬性或者設(shè)置了'secure=true'表示只能在 HTTPS 連接中傳遞cookies |
HttpOnly | 設(shè)置了該屬性或這設(shè)置了'HttpOnly=true'表示js腳本不能讀取到cookie信息 |
十、實(shí)現(xiàn)跨域讀取的方案
1.XMLHttpRequest的跨域
方案:
1.JSONP
2.CORS
3.WebSocket
4.代理
方案1:JSONP
只能用于Get請(qǐng)求卵迂,老式瀏覽器都支持。在網(wǎng)頁(yè)中創(chuàng)建一個(gè)<script>
標(biāo)簽,src
為請(qǐng)求的url
宝当,請(qǐng)求的查詢字符串有一個(gè)callback
參數(shù)庆揩,用來(lái)指定回調(diào)函數(shù)的名稱,回調(diào)函數(shù)在js腳本中聲明好订晌。當(dāng)服務(wù)器收到請(qǐng)求后蚌吸,返回一句js腳本锈拨,內(nèi)容是將json
數(shù)據(jù)作為參數(shù)傳入回調(diào)函數(shù)并調(diào)用該函數(shù)羹唠。
實(shí)例:
前端:
var jsonp = {
exec: function() {
var script = document.getElementById('jsonp');
if(script) {
script.parentElement.removeChild(script);
}
//創(chuàng)建<script>標(biāo)簽
script = document.createElement('script');
script.id = 'jsonp';
//返回js腳本:
//jsonp.jsonpcallback({"code":1000,"data":{"username":"carl","userAge":20,"userSex":"男"}})
script.src = 'http://localhost:8080/getusermsg?callback=jsonp.jsonpcallback';
document.head.appendChild(script);
},
//返回js腳本時(shí)會(huì)調(diào)用該函數(shù)
jsonpcallback: function (userdata) {
alert('姓名:' + userdata.data.username);
alert('年齡:'+ userdata.data.userAge);
alert('性別:' + userdata.data.userSex);
}
}
$('#btn1').click(jsonp.exec);
服務(wù)端:
function getusermsg(req, res, next) {
if(req.url.match(api.getusermsg)) {
var queryJson = queryParse(req.url.split('?')[1]);
var fb = {code: 1000, data: {username: 'carl', userAge: 20, userSex: '男'}};
//查詢字符串中有callback參數(shù)
if(queryJson.callback) {
res.writeHead(200);
//返回調(diào)用回調(diào)函數(shù)的字符串喊衫,前端以js腳本來(lái)解析并執(zhí)行
res.write(queryJson.callback + '(' + JSON.stringify(fb) + ')');
}else {
res.setHeader('Access-Control-Allow-Origin', '*');
res.writeHead(200);
res.write(JSON.stringify(fb));
}
}
next();
}
同樣也可以用jquery
來(lái)發(fā)起jsonp
請(qǐng)求族购,其原理也是跟上面一樣,只是對(duì)其進(jìn)行了一些封裝寝杖,調(diào)用起來(lái)更方便违施。因?yàn)槭墙柚?code><script>標(biāo)簽的src
屬性,JSONP
只能發(fā)GET
請(qǐng)求
方案2.CORS
CORS
是跨域資源分享的縮寫瑟幕,能夠徹底解決Ajax
的跨域問(wèn)題磕蒲,同時(shí)允許任意類型的請(qǐng)求留潦,需要服務(wù)器響應(yīng)頭中增加下面一種或幾種
//*表示允許任意源的訪問(wèn),也可以指定特定的源
1.Access-Control-Allow-Origin:*
//表示跨域訪問(wèn)時(shí)帶上cookie辣往,需同時(shí)在ajax請(qǐng)求中設(shè)置`withCredentials: true`兔院,
2.Access-Control-Allow-Credentials: true
//預(yù)檢請(qǐng)求后響應(yīng)的必須字段,返回所有支持的方法站削,而不單是瀏覽器請(qǐng)求的那個(gè)方
//法坊萝。這是為了避免多次"預(yù)檢"請(qǐng)求
3.Access-Control-Allow-Methods: GET, POST, PUT
//預(yù)檢請(qǐng)求后響應(yīng)的必須字段,放入預(yù)檢請(qǐng)求時(shí)請(qǐng)求所帶的頭
4.Access-Control-Allow-Headers:Content-Type
//允許瀏覽器在指定時(shí)間內(nèi)许起,無(wú)需再發(fā)送預(yù)檢請(qǐng)求進(jìn)行協(xié)商十偶,直接用本次協(xié)商結(jié)果即可
5.Access-Control-Max-Age: 1728000
CORS請(qǐng)求分為簡(jiǎn)單請(qǐng)求(HEAD、GET园细、POST)
和非簡(jiǎn)單請(qǐng)求(PUT或DELETE或Content-Type為application)
非簡(jiǎn)單請(qǐng)求會(huì)向發(fā)一個(gè)預(yù)檢請(qǐng)求(preflight)惦积,請(qǐng)求類型為OPTION,收到預(yù)檢請(qǐng)求的響應(yīng)后再發(fā)送真正的請(qǐng)求珊肃,這個(gè)時(shí)候的請(qǐng)求與簡(jiǎn)單請(qǐng)求無(wú)異荣刑。
簡(jiǎn)單地說(shuō)下CORS
請(qǐng)求會(huì)攜帶的頭信息
//必要請(qǐng)求頭,表示當(dāng)前源伦乔,相應(yīng)的預(yù)檢響應(yīng)需要返回Access-Control-Allow-Origin
1.Origin
//預(yù)檢時(shí)會(huì)帶上的頭厉亏,表示真正請(qǐng)求的方法,相應(yīng)的預(yù)檢響應(yīng)需要返回Access-Control-Allow-Method
2.Access-Control-Request-Method
//預(yù)檢時(shí)會(huì)帶上的頭烈和,表示真正請(qǐng)求會(huì)額外發(fā)送的頭信息爱只,相應(yīng)的預(yù)檢響應(yīng)需要返回Access-Control-Allow-Headers
3.Access-Control-Request-Headers
示例:
//1.簡(jiǎn)單請(qǐng)求
//前端
var cors = function() {
$.ajax(
{
url: 'http://localhost:8080/getusermsg',
type: 'GET',
dataType: 'json',
success: function(userdata) {
alert('姓名:' + userdata.data.username);
alert('年齡:'+ userdata.data.userAge);
alert('性別:' + userdata.data.userSex);
},
error: function(error) {
alert(JSON.stringify(error));
}
}
)
}
//服務(wù)端
function getusermsg(req, res, next) {
if(req.url.match(api.getusermsg)) {
var queryJson = queryParse(req.url.split('?')[1]);
var fb = {code: 1000, data: {username: 'carl', userAge: 20, userSex: '男'}};
if(queryJson.callback) {
res.writeHead(200);
res.write(queryJson.callback + '(' + JSON.stringify(fb) + ')');
}else {
//允許任意源訪問(wèn)
res.setHeader('Access-Control-Allow-Origin', '*');
res.writeHead(200);
res.write(JSON.stringify(fb));
}
}
next();
}
//2.非簡(jiǎn)單請(qǐng)求
//前端
var postUser = function() {
$.ajax({
url: 'http://localhost:8080/postusermsg',
type: 'POST',
//發(fā)送json數(shù)據(jù)觸發(fā)預(yù)檢preflight,option請(qǐng)求
contentType: 'application/json',
data: {username: 'jjp', userAge: 22, userSex: '男'},
dataType: 'json',
success: function(data) {
if(data.code === 1000) {
alert('添加用戶成功')
}
},
error: function(err) {
alert(JSON.stringify(err));
}
})
}
//服務(wù)端
function postusermsg(req, res, next) {
if(req.url.match(api.postusermsg)) {
var body = '';
req.on('data', function(chunk){
body += chunk;
});
req.on('end', function(){
var queryJson = queryParse(body);
console.log(queryJson);
//針對(duì)預(yù)檢請(qǐng)求發(fā)送的頭為響應(yīng)設(shè)置相對(duì)應(yīng)的頭
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
res.setHeader('Access-Control-Allow-Method', 'GET,POST,PUT');
res.writeHead(200);
res.write(JSON.stringify({code:1000, data:{}}));
next();
});
}else {
next();
}
}
方案3.WebSocket
WebSocket
是一種新的通信協(xié)議招刹,能夠在一個(gè)持久連接上提供全雙工恬试、雙向通信。使用url模式也略有不同疯暑。未加密連接使用ws://
训柴,加密連接使用wss://
,最重要的一點(diǎn)是該協(xié)議不實(shí)行同源策略妇拯。服務(wù)器需要自己確定請(qǐng)求源是否在白名單內(nèi)幻馁,從而過(guò)濾惡意的請(qǐng)求。
方案4.代理
1.正向代理:需要借助同源的代理服務(wù)器越锈,瀏覽器先將請(qǐng)求發(fā)送給代理服務(wù)器仗嗦,代理服務(wù)器接收請(qǐng)求并其轉(zhuǎn)發(fā)給目標(biāo)數(shù)據(jù)服務(wù)器,由于不同源的兩個(gè)服務(wù)器的交互不遵循同源策略甘凭,所以代理服務(wù)器可以接收到目標(biāo)數(shù)據(jù)服務(wù)器的響應(yīng)數(shù)據(jù)稀拐,再將響應(yīng)數(shù)據(jù)發(fā)送回瀏覽器
2.反向代理:通常可以用Nginx反向代理來(lái)實(shí)現(xiàn)丹弱,也是利用了服務(wù)端之間的資源交互不會(huì)有跨域限制的原理德撬。假如現(xiàn)在有
domainA
:http://jjp.com:3000
铲咨,部署了請(qǐng)求頁(yè)面
domainB
:http://jjp.com:8080
,部署了nginx服務(wù)器
domainC
:http://jjp.com:6600
砰逻,部署了資源服務(wù)器
瀏覽器獲取了domainA
的頁(yè)面鸣驱,然后用ajax
向domainC
請(qǐng)求數(shù)據(jù),必然會(huì)發(fā)生跨域蝠咆。所以domainA
可以讓nginx
做反向代理服務(wù)器,讓nginx
服務(wù)器假扮domainA
北滥,此時(shí)瀏覽器請(qǐng)求domainB
時(shí)會(huì)得到原先部署在domainA
上的頁(yè)面刚操。瀏覽器想請(qǐng)求domainC
的資源時(shí),直接向domainB
發(fā)請(qǐng)求即可再芋,nginx
服務(wù)器會(huì)攔截瀏覽器的請(qǐng)求菊霜,重寫為向domainC
請(qǐng)求,再轉(zhuǎn)發(fā)該請(qǐng)求济赎,nginx
拿到資源后返回給瀏覽器
2.Cookie的跨域
同源的頁(yè)面才可以共享cookie
鉴逞,但是如果兩個(gè)源的一級(jí)域名相同,二級(jí)域名不同司训,瀏覽器可以通過(guò)設(shè)置document.domain
來(lái)共享cookie
构捡,比如有
domainA
:http://gg.jjp.com/index.html
domainB
:http://bb.jjp.com/index.html
現(xiàn)在想讓domainA
和domainB
能互相訪問(wèn)對(duì)方的cookie
,可以雙方都設(shè)置document.domain
為jjp.com
壳猜,domainA
則能夠訪問(wèn)到domainB
設(shè)置的cookie
勾徽,domainB
也能訪問(wèn)到domainA
設(shè)置的cookie
.
3.跨窗口的跨域通信
iframe窗口和window.open打開的窗口若與父窗口不是同源的,都無(wú)法與創(chuàng)建它們的父窗口通信统扳,無(wú)法互相訪問(wèn)對(duì)方的document
對(duì)象喘帚。如果兩個(gè)窗口一級(jí)域名相同,二級(jí)域名不同咒钟,可以通過(guò)設(shè)置document.domain
解決吹由。
但是對(duì)于完全不同源的窗口,想要進(jìn)行通信朱嘴,可以通過(guò)下面的方法:
1.片段識(shí)別符
2.window.name
3.window.postMessage
方案1.片段識(shí)別符
地址欄中url
的#
后面的內(nèi)容變化是不會(huì)引起頁(yè)面的刷新的倾鲫,這部分內(nèi)容就是片段識(shí)別符,當(dāng)片段識(shí)別符內(nèi)容變化時(shí)腕够,會(huì)觸發(fā)hashchange
事件级乍。
因此發(fā)信息的窗口可以把信息寫入接收信息窗口的片段標(biāo)識(shí)符中,接收信息窗口監(jiān)聽hashchange
事件來(lái)取得自己的片段標(biāo)識(shí)符帚湘,從而來(lái)達(dá)成通信的目的玫荣。
方案2.window.name
window.name
值在不同的頁(yè)面(甚至不同域名)加載后依舊存在,并且值最大可以達(dá)到2MB大诸。
示例:窗口A和窗口B不同源捅厂,現(xiàn)在A想拿到窗口B的消息贯卦,可以借助window.name 以及 iframe實(shí)現(xiàn)跨域通信
步驟:
1.窗口A在頁(yè)面中動(dòng)態(tài)添加一個(gè)iframe,將其src置為窗口B頁(yè)面地址
2.iframe加載了窗口B的頁(yè)面焙贷,窗口B將要發(fā)送的消息寫入window.name中
3.由于窗口A與iframe處于不同域撵割,因?yàn)橥床呗裕翱贏不能訪問(wèn)iframe的window.name
4.此時(shí)再讓iframe加載一個(gè)與窗口A同源的頁(yè)面辙芍,使窗口A與iframe屬于同域
5.窗口A讀取iframe的window.name啡彬,至此接收到窗口B發(fā)送的消息,完成通信
方案3.window.postMessage
window.postMessage
是HTML5
引入的一個(gè)新的api故硅,允許兩個(gè)窗口通信庶灿,不論是否兩個(gè)窗口是否同源
示例:
//發(fā)送信息的窗口:http://jjp.com
var sonWin = window.open('https://www.baidu.com','百度');
//參數(shù):要發(fā)送的信息、接受信息的窗口的源
sonWin.postMessage('你好吃衅,百度', 'https://www.baidu.com');
//接收信息的窗口:https://www.baidu.com
//監(jiān)聽postMessage事件
window.addEventListener('message', function(event) {
//event.source:發(fā)送消息的窗口
//event.origin: 發(fā)送消息的網(wǎng)址
//event.data: 消息內(nèi)容
if(event.origin === 'http://jjp.com') {
event.source.postMessage('Got it', event.origin);
console.log(event.data)
}
});
4.跨源數(shù)據(jù)存儲(chǔ)
通過(guò)window.postMessage
往踢,能夠?qū)崿F(xiàn)讀寫其他窗口的localStorage
和IndexDB
。
在用window.postMessage
實(shí)現(xiàn)窗口間的通信的基礎(chǔ)上進(jìn)行
- 寫:接收其他窗口的消息時(shí)徘层,將消息作為值其存入
- 讀:接受其他窗口的消息時(shí)峻呕,將消息作為鍵值取出值,并將值通過(guò)
postMessage
發(fā)送給其他窗口