什么是同源策略
同源策略/SOP(Same origin policy)是一種約定岛抄,由Netscape公司1995年引入瀏覽器绅络,它是瀏覽器最核心也最基本的安全功能月培,如果缺少了同源策略嘁字,瀏覽器很容易受到XSS、CSFR等攻擊杉畜。
所謂同源是指"協(xié)議+域名+端口" 三者相同纪蜒,即便兩個(gè)不同的域名指向同一個(gè)ip地址,也非同源此叠。
同源策略限制以下幾種行為:
1.) Cookie纯续、LocalStorage 和 IndexDB 無法讀取
2.) DOM 和 Js對象無法獲得
3.) AJAX 請求不能發(fā)送
本域指的是
- 同協(xié)議:如都是http或者h(yuǎn)ttps
- 同域名:如都是http://jirengu.com/a 和http://jirengu.com/b
- 同端口:如都是80端口
例子
不同源的例子:
- http://jirengu.com/main.js 和 https://jirengu.com/a.php (協(xié)議不同)
- http://jirengu.com/main.js 和 http://bbs.jirengu.com/a.php (域名不同,域名必須完全相同才可以)
- http://jiengu.com/main.js 和 http://jirengu.com:8080/a.php (端口不同,第一個(gè)是80)
需要注意的是: 對于當(dāng)前頁面來說頁面存放的 JS 文件的域不重要灭袁,重要的是加載該 JS 頁面所在什么域
什么是跨域猬错?跨域有幾種實(shí)現(xiàn)形式
廣義的跨域:
- 資源跳轉(zhuǎn): A鏈接、重定向茸歧、表單提交
- 資源嵌入:
<link>倦炒、<script>、<img>软瞎、<frame>
等dom標(biāo)簽逢唤,還有樣式中background:url()、@font-face()等文件外鏈 - 腳本請求: js發(fā)起的ajax請求涤浇、dom和js對象的跨域操作等
其實(shí)我們通常所說的跨域是狹義的智玻,是由瀏覽器同源策略限制的一類請求場景。
跨域解決方案
- 通過jsonp跨域
- document.domain + iframe跨域(降域)
- location.hash + iframe
- window.name + iframe跨域
- postMessage跨域
- 跨域資源共享(CORS)
- nginx代理跨域
- nodejs中間件代理跨域
- WebSocket協(xié)議跨域
1. JSONP
簡易例子
請求方:frank.com的前端程序員(瀏覽器)
響應(yīng)方:jack.com的后端程序員(服務(wù)器)
- 請求方創(chuàng)建 script芙代,src指向響應(yīng)方,同時(shí)傳一個(gè)查詢參數(shù)
?callbackName=xxx
- 響應(yīng)方根據(jù)查詢參數(shù)callbackName,構(gòu)造形如
- xxx.call(undefined,'所需的數(shù)據(jù)')
- xxx('所需的數(shù)據(jù)')
- 瀏覽器接收到相應(yīng)盖彭,就會(huì)執(zhí)行xxx.call(undefined,'所需的數(shù)據(jù)')
- 那么請求方就知道他所需要的數(shù)據(jù)
query為temp.query,提取請求URL中的哈希值
約定
- callbackName 一般用 callback
- xxx 一般用 隨機(jī)數(shù) 例如jq1253156(數(shù)值隨機(jī)纹烹,避免函數(shù)名重復(fù))
通常為了減輕web服務(wù)器的負(fù)載,我們把js召边、css铺呵,img等靜態(tài)資源分離到另一臺(tái)獨(dú)立域名的服務(wù)器上,在html頁面中再通過相應(yīng)的標(biāo)簽從不同域名下加載靜態(tài)資源隧熙,而被瀏覽器允許片挂,基于此原理,我們可以通過動(dòng)態(tài)創(chuàng)建script贞盯,再請求一個(gè)帶參網(wǎng)址實(shí)現(xiàn)跨域通信音念。
jsonp缺點(diǎn):只能實(shí)現(xiàn)get一種請求。
步驟
- 定義數(shù)據(jù)處理函數(shù)onBack()
- 創(chuàng)建script標(biāo)簽躏敢,src的地址執(zhí)行后端接口闷愤,最后加個(gè)參數(shù)callback=onBack
- 服務(wù)端在收到請求后,解析參數(shù)件余,計(jì)算返還數(shù)據(jù)讥脐,輸出 onBack(date)字符串遭居。
- onBack(date)會(huì)放到script標(biāo)簽做為js執(zhí)行。此時(shí)會(huì)調(diào)用onBack函數(shù)旬渠,將data做為參數(shù)俱萍。
JSONP為何不支持POST??
- 因?yàn)镴SONP是通過動(dòng)態(tài)創(chuàng)建script實(shí)現(xiàn)
- script無法支持POST
詳細(xì)
把<script>
元素的src屬性設(shè)成一個(gè)回傳JSON的URL是可以想像的,這也代表從HTML頁面通過script元素抓取 JSON是可能的告丢。
然而枪蘑,一份JSON文件并不是一個(gè)JavaScript程序。為了讓瀏覽器可以在<script>
元素運(yùn)行芋齿,從src里URL 回傳的必須是可運(yùn)行的JavaScript腥寇。在JSONP的使用模式里,該URL回傳的是由函數(shù)調(diào)用包起來的動(dòng)態(tài)生成JSON觅捆,這就是JSONP的“填充(padding)”或是“前輟(prefix)”的由來赦役。
慣例上瀏覽器提供回調(diào)函數(shù)的名稱當(dāng)作送至服務(wù)器的請求中命名查詢參數(shù)的一部分衫哥,例如:
<script>
var script = document.createElement('script');
script.type = 'text/javascript';
// 傳參并指定回調(diào)執(zhí)行函數(shù)為onBack
script.src = 'http://www.domain2.com:8080/login?user=admin&callback=onBack';
document.head.appendChild(script);
// 回調(diào)執(zhí)行函數(shù)
function onBack(res) {
alert(JSON.stringify(res));
}
</script>
服務(wù)器會(huì)在傳給瀏覽器前將JSON數(shù)據(jù)填充到回調(diào)函數(shù)(onBack)中其骄。瀏覽器得到的回應(yīng)已不是單純的數(shù)據(jù)敘述而是一個(gè)腳本探橱。在本例中屁柏,瀏覽器得到的是:
onBack({"Name": "小明", "Id" : 1823, "Rank": 7})
后端node.js代碼示例:
var querystring = require('querystring');
var http = require('http');
var server = http.createServer();
server.on('request', function(req, res) {
var params = qs.parse(req.url.split('?')[1]);
var fn = params.callback;
// jsonp返回設(shè)置
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.write(fn + '(' + JSON.stringify(params) + ')');
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
2. CORS
CORS 全稱是跨域資源共享(Cross-Origin Resource Sharing)跟啤,是一種 ajax 跨域請求資源的方式赊时,支持現(xiàn)代瀏覽器吕粗,IE支持10以上互婿。
實(shí)現(xiàn)方式很簡單释移,當(dāng)你使用 XMLHttpRequest 發(fā)送請求時(shí)叭披,瀏覽器發(fā)現(xiàn)該請求不符合同源策略,會(huì)給該請求加一個(gè)請求頭:Origin玩讳,后臺(tái)進(jìn)行一系列處理涩蜘,如果確定接受請求則在返回結(jié)果中加入一個(gè)響應(yīng)頭:Access-Control-Allow-Origin; 瀏覽器判斷該相應(yīng)頭中是否包含 Origin 的值,如果有則瀏覽器會(huì)處理響應(yīng)熏纯,我們就可以拿到響應(yīng)數(shù)據(jù)同诫,如果不包含瀏覽器直接駁回,這時(shí)我們無法拿到響應(yīng)數(shù)據(jù)樟澜。
所以 CORS 的表象是讓你覺得它與同源的 ajax 請求沒啥區(qū)別误窖,代碼完全一樣。
此方法的原理是:
- 在服務(wù)器上添加下面語句秩贰,告訴瀏覽器霹俺,除了同源網(wǎng)址外,還接收什么網(wǎng)址訪問
res.header("Access-Control-Allow-Origin", "http://a.jrg.com:8080");
//允許http://a.jrg.com:8080 端口訪問
res.header("Access-Control-Allow-Origin", "*"); //允許所有其他端口訪問
3. 降域
例如有a(a.jinrengu.com)毒费、b(b.jinrengu.com)兩個(gè)網(wǎng)址吭服,現(xiàn)在需要用a訪問b網(wǎng)址,由于兩個(gè)網(wǎng)址不一樣蝗罗,故不同源艇棕,瀏覽器阻止訪問b網(wǎng)址蝌戒。
此方案僅限主域相同,子域不同的跨域應(yīng)用場景沼琉。
原理:實(shí)現(xiàn)原理:兩個(gè)頁面都通過js強(qiáng)制設(shè)置document.domain為基礎(chǔ)主域北苟,就實(shí)現(xiàn)了同域。
1.)父窗口:(http://a.yuanqianyi.com:8080/a.html)
<html>
<style>
.ct{
width: 910px;
margin: auto;
}
.main{
float: left;
width: 450px;
height: 300px;
border: 1px solid #ccc;
}
.main input{
margin: 20px;
width: 200px;
}
.iframe{
float: right;
}
iframe{
width: 450px;
height: 300px;
border: 1px dashed #ccc;
}
</style>
<div class="ct">
<h1>使用降域?qū)崿F(xiàn)跨域</h1>
<div class="main">
<input type="text" placeholder="http://a.yuanqianyi.com:8080/a.html">
</div>
<iframe src="http://b.yuanqianyi.com:8080/b.html" frameborder="0" ></iframe>
</div>
<script>
document.querySelector('.main input').addEventListener('input', function(){
console.log(this.value);
window.frames[0].document.querySelector('input').value = this.value;
})
document.domain = "yuanqianyi.com"
</script>
</html>
2.)子窗口:(http://b.yuanqianyi.com:8080/b.html)
<html>
<style>
html,body{
margin: 0;
}
input{
margin: 20px;
width: 200px;
}
</style>
哇哇哇哇
<input id="input" type="text" placeholder="http://b.yuanqianyi.com:8080/b.html">
<script>
document.querySelector('#input').addEventListener('input', function(){
window.parent.document.querySelector('input').value = this.value;
})
document.domain = 'yuanqianyi.com';
</script>
</html>
4. postMessage跨域
postMessage是HTML5 XMLHttpRequest Level 2中的API打瘪,且是為數(shù)不多可以跨域操作的window屬性之一友鼻,它可用于解決以下方面的問題:
- 頁面和其打開的新窗口的數(shù)據(jù)傳遞
- 多窗口之間消息傳遞
- 頁面與嵌套的iframe消息傳遞
- 上面三個(gè)場景的跨域數(shù)據(jù)傳遞
- 用法:postMessage(data,origin)方法接受兩個(gè)參數(shù)
- data: html5規(guī)范支持任意基本類型或可復(fù)制的對象,但部分瀏覽器只支持字符串闺骚,所以傳參時(shí)最好用JSON.stringify()序列化彩扔。
- origin: 協(xié)議+主機(jī)+端口號(hào),也可以設(shè)置為"*"僻爽,表示可以傳遞給任意窗口虫碉,如果要指定和當(dāng)前窗口同源的話設(shè)置為"/"。
- a.html: (http://a.yuanqianyi.com:8080/a.html)
//向b發(fā)送跨域請求
document.querySelector('.main input').addEventListener('input', function(){
console.log(this.value)
window.frames[0].postMessage(this.value,"*")
})
//接收b返回的數(shù)據(jù)
window.addEventListener("message",function(e){
document.querySelector('.main input').value = e.data
console.log(e.data)
})
- b.html:(http://b.yuanqianyi.com:8080/b.html)
// 接收a的數(shù)據(jù)請求
window.addEventListener("message",function(e){
document.querySelector('input').value = e.data
console.log(e.data)
})
//發(fā)送數(shù)據(jù)給a
document.querySelector('input').addEventListener('input', function(){
window.parent.postMessage(this.value,"*")
window.parent.document.querySelector('input').value = this.value;
})