一、瀏覽器的同源策略
1.什么是同源?
所謂“同源”指的是”三個相同“。相同的域名、端口和協議,這三個相同的話就視為同一個域,本域下的JS腳本只能讀寫本域下的數據資源,無法訪問其它域的資源。
協議相同
域名相同
端口相同(如果沒有寫端口,默認是80端口)
2.什么是同源策略?
同源策略是瀏覽器為了保護用戶的個人信息以及企業數據的安全而設置的一種策略,不同源的客戶端腳本是不能在對方未允許的情況下訪問或索取對方的數據信息;
3.同源策略的目的
同源政策的目的,是為了保證用戶信息的安全,防止惡意的網站竊取數據。
設想這樣一種情況:A 網站是一家銀行,用戶登錄以后,又去瀏覽其他網站。如果其他網站可以讀取 A 網站的 Cookie,會發生什么?
很顯然,如果 Cookie 包含隱私(比如存款總額),這些信息就會泄漏。更可怕的是,Cookie 往往用來保存用戶的登錄狀態,如果用戶沒有退出登錄,其他網站就可以冒充用戶,為所欲為。因為瀏覽器同時還規定,提交表單不受同源政策的限制。
由此可見,“同源政策”是必需的,否則 Cookie 可以共享,互聯網就毫無安全可言了。
4.非同源的限制
隨著互聯網的發展,“同源政策”越來越嚴格。目前,如果非同源,共有三種行為受到限制。
(1) Cookie、LocalStorage 和 IndexedDB 無法讀取。
(2) DOM 無法獲得。
(3) AJAX 請求無效(可以發送,但瀏覽器會拒絕接受響應)。
5.什么是跨域?
跨域就是解決同源策略帶來的不便,突破同源策略的限制去獲取不同源之間的數據信息或者進行不同源之間的信息傳遞。
二、跨域的幾種實現方法
1. JSONP
1.1什么是JSONP
JSONP是JSON with padding(填充式JSON或參數式JSON)的簡寫,是應用JSON的一種新方法。JSONP看起來與JSON差不多,只不過是被包含在函數調用中的JSON,就像下面這樣。
callback({ "name": "Nicholas" });
JSONP由兩部分組成:回調函數和數據。回調函數是當響應到來時應該在頁面中調用的函數。回調函數的名字一般是在請求中指定的。而數據就是傳入回調函數的JSON數據。
簡單來說就是利用并提供一個回調函數來接收數據(函數名可約定),響應傳到來時傳遞過來的數據為json數據的包裝(故稱之為jsonp,即json padding)。 傳過來的數據類似: callback({"name":"hax","gender":"Male"})
。因為JSONP是有效的JavaScript代碼,所以在請求完成后,即在JSONP響應加載到頁面中以后,瀏覽器會立即執行callback函數,并傳遞解析后的json對象作為參數。
1.2JSONP的原理
jsonp其實就是利用<script>元素本身可跨域,可以將其src屬性里指定的路徑里的資源下載下來的設定,從而達到跨域的目的。
JSONP是通過動態創建<script>元素來使用的,使用時為src屬性指定一個跨域URL。<script>元素與<img>元素類似,都有能力不受限制地從其他域加載資源。因為JSONP是有效的JavaScript代碼,所以在請求完成后,即在JSONP響應加載到頁面中以后,就會立即執行。
1.3JSONP的使用步奏
本域:
- 首先動態創建script標簽;
var script = document.createElement('script');
- 創建回調函數callback(假定函數名為appendHtml),然后將該函數與callback字段結合成鍵值對的形式,例如:callback=appendHtml,接著將其與要訪問的遠端(不同源)的接口url(假設要訪問的url為http://localhost:8080)結合成如下形式:
script.src = 'http://localhost:8080/getNews?callback=appendHtml';
服務器部分:
獲取到回調函數appendHtml后,把需要發送的數據與函數appendHtml進行包裝,使用字符串拼接的方式組成如下形式再發回給本域:
aaa({"name": "xiaoming", "age": "1000"});
請求完成后后:瀏覽器會調用回調函數appendHtml,把獲得的數據以參數形式傳遞進去,進行數據處理;
PS:由上述步驟可見,jsonp的使用是需要遠端支持的。
1.4JSONP的優缺點
優點:
- 簡單易用,能夠直接訪問響應文本,支持在瀏覽器與服務器之間雙向通信。
缺點:
因為src屬性自己獲取數據要在url后面加上數據參數,那么這個方式就只有get,所以JSONP也只能用get方式獲取數據;
JSONP只能解決跨域獲取資源問題,但是不能解決不同域頁面之間的JS調用問題;
安全性問題:由于JSONP是從其他域中加載代碼執行,如果其他域不安全,很可能會在響應中夾帶一些惡意代碼,而此時除了完全放棄JSONP調用之外,沒有辦法追究;
要半段JSONP請求失敗并不容易,它不會像ajax那樣如果失敗的話會返回失敗的http狀態碼;
2.CORS
2.1什么是CORS?
CORS是一個W3C標準,全稱是“跨域資源共享”(Cross-origin resource sharing)。它允許瀏覽器向跨源服務器,發出XMLHttpRequest
請求,從而克服了AJAX只能同源使用的限制。
CORS需要瀏覽器和服務器同時支持。整個CORS通信過程,都是瀏覽器自動完成,不需要用戶參與。對于開發者來說,CORS通信與同源的AJAX通信沒有差別,代碼完全一樣。瀏覽器一旦發現AJAX請求跨域,就會自動添加一些附加的頭信息。
因此,實現CORS通信的關鍵是服務器。只要服務器實現了CORS接口,就可以跨域通信。
2.2CORS的原理
如果瀏覽器發現這次是跨域的AJAX
請求,就會在請求頭信息之中,增加一個Origin
字段。Origin
字段用來說明,本次請求來自哪個源(協議 + 域名 + 端口)。服務器根據這個值,決定是否同意這次請求。
如果Origin
指定的源,不在許可范圍內,服務器會返回一個正常的HTTP
回應。瀏覽器發現,這個回應的頭信息沒有包含Access-Control-Allow-Origin
字段,就知道出錯了,從而拋出一個錯誤,被XMLHttpRequest
的onerror
回調函數捕獲。
如果Origin指定的域名在許可范圍內,服務器返回的響應,會多出響應頭信息字段。
Access-Control-Allow-Origin: Origin的值,即本次請求來源哪個源(協議 + 域名 + 端口)。
Access-Control-Allow-Origin
該字段是必須的。它的值要么是請求時Origin字段的值,要么是一個*,表示接受任意域名的請求。
2.3CORS的實現方式
CORS通信的實現只能依賴跨域服務器的支持,而在本域下的的代碼只是普通的AJAX請求。
通過在跨域服務器中對響應頭進行設置,實現對指定的域允許進行數據通信,如下代碼是對響應頭進行的設置:
header("Access-Control-Allow-Origin", "http://a.jrg.com:8080")
這個代碼實現了 http://a.jrg.com:8080
對其數據的訪問;
2.4CORS跨域的實現步奏
本域:發出普通的AJAX請求
跨域服務器:在服務器端通過設置
header
屬性來指定允許跨域的源地址。如:header("Access-Control-Allow-Origin","http://a.jrg.com:8080");
,表明允許http://a.jrg.com:8080
這個源地址獲取數據。在AJAX請求發過來之后,如果發送AJAX請求的地址是http://a.jrg.com:8080
,則在返回的數據中添加響應頭信息header('Access-Control-Allow-Origin', '允許跨域進行訪問的域名')
,這里是header("Access-Control-Allow-Origin","http://a.jrg.com:8080"),如果在服務器端header("Access-Control-Allow-Origin","*");
這么設置,表明允許所有的域都可以對其進行跨域訪問。本域分兩種情況:
1、已經被允許跨域訪問:在響應頭處出現一個鍵值對,如:Access-Control-Allow-Origin: http://a.com:8080
。
2、未允許進行跨域訪問:
①:可能是跨域服務器不支持CORS跨域訪問,那么就不會有類似Access-Control-Allow-Origin: http://a.com:8080
的響應頭信息。
②:跨域服務器不支持本域進行訪問,也會有回應頭信息,該信息標注那些域是可以進行訪問的,比如:跨域服務器支持a.com
進行訪問,而我用b.com
對其進行訪問,回應頭就會回復Access-Control-Allow-Origin: http://a.com:8080
字段,表明只有a.com是支持訪問的;出現如下錯誤:
2.5示例代碼
本域:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>news</title>
<style>
.container{
width: 900px;
margin: 0 auto;
}
</style>
</head>
<body>
<div class="container">
<ul class="news">
<li>第11日前瞻:中國沖擊4金 博爾特再戰200米羽球</li>
<li>男雙力爭會師決賽</li>
<li>女排將死磕巴西!</li>
</ul>
<button class="change">換一組</button>
</div>
<script>
$('.change').addEventListener('click',function(){
var xhr = new XMLHttpRequest();
//向http://b.jrg.com:8080地址請求數據,本域的地址為http://a.jrg.com:8080
xhr.open('get','http://b.jrg.com:8080/getNews',true);
xhr.send();
xhr.onreadystatechange = function(){
if(xhr.readyState === 4){
if (xhr.status === 200 || xhr.status === 304) {
appendHtml(JSON.parse(xhr.responseText))
}
}
}
})
function appendHtml(news){
var html = '';
for(var i = 0; i<news.length;i++){
html +='<li>' + news[i] + '</li>';
}
console.log(html);
$('.news').innerHTML = html;
}
function $(id){
return document.querySelector(id)
}
</script>
</body>
</html>
服務器部分:
app.get('/getNews',function(req,res){
var news = [
"1.第11日前瞻:中國沖擊4金 博爾特再戰200米羽球",
"2.正直播柴彪、宏偉出站",
"3.女排將死磕巴西",
"4.沒有中國選手和巨星的110米欄 我們還看嗎",
"5.中英上演奧運會金牌大戰",
"6.博彩賠率挺中國奪回第二紐約時報",
"7.最”出柜奧運?“",
"8.下跪與洪荒之力"
]
var data = [];
for(var i = 0; i < 3; i++){
var index = parseInt(Math.random()*news.length);
data.push(news[index]);
news.splice(index,1);
}
//通過設置屬性header,允許來自http://a.jrg.com:8080地址的AJAx請求
res.header("Access-Control-Allow-Origin","http://a.jrg.com:8080");
//允許來自任何域、的AJAx請求
//res.header("Access-Control-Allow-Origin","*")
res.send(data);
})
2.6CORS與JSON的對比
jsonp比CORS優秀的地方:
- jsonp兼容性較好,而CORS在IE中只兼容IE10以上瀏覽器,此外在IE7或以下的IE瀏覽器中,因為沒有XMLHttpRequest對象,只支持ActiveX對象,所以注定無法使用CORS,而jsonp這時候就可以大放異彩;
CORS比jsonp優秀的地方:
CORS在前端部分只需要發送普通的AJAX請求即可,使用起來和不跨域時并無不同,更加的方便;
因為第一條,所以CORS支持其它的請求方式(比如post、put等);
如何選擇:
- 在有選擇的情況下,兼容老瀏覽器可以使用jsonp,主流瀏覽器可以選用CORS;
3.降域
3.1什么是降域
降域就是當兩個一級域名相同但二級域名不同時(如:a.xgj.com和b.xgj.com中一級都是xgj.com,a和b是主機名),對兩個域名都設置document.domain = 一級域名來達到跨域的目的;
3.2降域的限制性
使用降域來達成跨域的目的有非常大的限制性:
主域名要相同:a.com和b.com就不行,a.oxc.com和b.oxc.com就可以;
降域只適用于iframe窗口和獲取cookie,但不能獲取LocalStorage 和 IndexDB ;
3.3降域例子
實現功能:當在a.xgj.com的輸入框中輸入字符,b.xgj.com的輸入框中也會出現相同字符
在a頁面(a.xgj.com頁面)使用<iframe>嵌入b頁面(b.xgj.com頁面)
a頁面代碼:
<body>
<div class="ct">
<h1>使用降域實現跨域</h1>
<div class="main">
<input type="text" placeholder="http://a.xgj.com:8080/a.html">
</div>
<iframe src="http://b.xgj.com:8080/b.html" frameborder="0"></iframe>
<script>
document.querySelector('.main input').addEventListener('input',function(){
console.log(this.value);
//先用window.frames[0]獲取iframe節點,因為iframe加載的是另一個html所以也有document,之后用document.querySelector('input')獲取input節點
/*如果文檔包含框架(frame 或 iframe 標簽),瀏覽器會為 HTML 文檔創建一個 window 對象,并為每個框架創建一個額外的 window 對象。
window.frames:返回窗口中所有命名的框架。
該集合是 Window 對象的數組,每個 Window 對象在窗口中含有一個框架或 <iframe>。屬性 frames.length 存放數組 frames[] 中含有的元素個數。注意,frames[] 數組中引用的框架可能還包括框架,它們自己也具有 frames[] 數組。
*/
window.frames[0].document.querySelector('input').value = this.value;
})
document.domain = "jrg.com";
</script>
</body>
b頁面代碼:
<body>
<input type="text" id="input" placeholder="http://b.jrg.com">
<script>
document.querySelector('#input').addEventListener('input',function(){
window.parent.document.querySelector('input').value = this.value;
})
document.domain = 'jrg.com';
</script>
</body>
效果圖:
4、postMessage
4.1什么是postMessage?
HTML5為了解決跨域問題,引入了一個全新的API:跨文檔通信 API(Cross-document messaging)。
這個API為window對象新增了一個window.postMessage方法,允許跨窗口通信,不論這兩個窗口是否同源。
舉例來說,父窗口aaa.com向子窗口bbb.com發消息,調用postMessage方法就可以了。
var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://bbb.com');
postMessage方法的第一個參數是具體的信息內容,第二個參數是接收消息的窗口的源(origin),即“協議 + 域名 + 端口”。也可以設為*,表示不限制域名,向所有窗口發送。
4.2postMessage使用例子
實現功能:當在a.xgj.com的輸入框中輸入字符,b.xgj.com的輸入框中也會出現相同字符;
a頁面:
<body>
<div class="ct">
<h1>使用postMessage實現跨域</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>
$('.main input').addEventListener('input',function(){
console.log(this.value);
//給iframe發消息
window.frames[0].postMessage(this.value,'*');
})
//接收別人的消息要去監聽message事件
window.addEventListener('message',function(e){
$('.main input').value = e.data;
console.log(e.data);
});
function $(id){
return document.querySelector(id);
}
</script>
</body>
b頁面:
<body>
<input type="text" id="input" placeholder="http://b.jrg.com:8080/b.html">
<script>
$('#input').addEventListener('input',function(){
window.parent.postMessage(this.value,'*');
})
window.addEventListener('message',function(e){
$('#input').value = e.data;
console.log(e.data);
})
function $(id){
return document.querySelector(id);
}
</script>
</body>