同源策略與JS跨域
為什麼要跨域
為了使用者的資訊保安,瀏覽器就引入了同源策略
那麼同源策略是如何保證使用者的資訊保安的呢?
- 如果沒有同源策略,你打開了你的銀行賬戶頁面A,又打開了另一個不相關的頁面B,這時候如果B是惡意網站,B可以通過Javascript輕鬆訪問和修改A頁面中的內容
- 現在我們廣泛的使用cookie來維護使用者的登入狀態,而如果沒有同源策略,這些cookie資訊就會洩露,其他網站就可以冒充這個登入使用者
由此可以看出,同源策略確實是必不可少的,那麼它會帶來哪些限制呢?
- Cookie、LocalStorage和IndexDB無法讀取
- DOM無法獲得
- AJAX請求不能傳送
有時候我們需要突破上述限制,就需要用跨域的方法來解決
跨域是什麼?
- 什麼叫做不同的域?
協議(http)、域名(www.a.com)、埠(8000)三者中有一個不同就叫不同的域 - 跨域就是不同的域間相互訪問時使用某些方法來突破上述限制
- 協議或者埠的不同,只能通過後臺來解決
如何實現跨域?
一、解決上面提到的1、2兩點限制:
1. 通過document.domain跨子域
適用範圍:
- 兩個域只是子域不同
- 只適用於iframe視窗與父視窗之間互相獲取cookie和DOM節點,不能突破LocalStorage和IndexDB的限制
當兩個不同的域只是子域不同時,可以通過把document.domain設定為他們共同的父域來解決
當A、B想要獲取對方的cookie
DOM節點
時,可以設定:
document.domain=‘example.com’;
這時A網頁通過指令碼設定:
document.cookie = “testA=hello”;
B網頁就可以拿到這個cookie:
var aCookie = document.cookie;
2. 通過window.name跨域
使用範圍:
- 可以是兩個完全不同源的域
- 同一個視窗內:即同一個標籤頁內先後開啟的視窗
window.name屬性有個特徵:即在一個視窗(window)的生命週期內,視窗載入的所有的頁面都是共享一個window.name的,每個頁面對window.name都有讀寫的許可權,window.name是持久存在一個視窗載入過的所有頁面中的。
基於這個思想,我們可以在某個頁面設定好 window.name 的值,然後在本標籤頁內跳轉到另外一個域下的頁面。在這個頁面中就可以獲取到我們剛剛設定的 window.name 了。
結合iframe還有更高階的用法:
父視窗先開啟一個與自己不同源的子視窗,在這個子窗口裡設定:
然後讓子視窗跳轉到一個與父視窗同域的網址:
var data = document.getElementById(‘myFrame’).contentWindow.name;
優點:window.name容量很大,可以放置非常長的字串;缺點:必須監聽子視窗window.name屬性的變化,影響網頁效能。
3. 使用HTML5的window.postMessage跨域
window.postMessage(message,targetOrigin) 方法是html5新引進的特性,可以使用它來向其它的window物件傳送訊息,無論這個window物件是屬於同源或不同源,目前IE8+、FireFox、Chrome、Opera等瀏覽器都已經支援window.postMessage方法。
otherWindow.postMessage(message, targetOrigin);
otherWindow:接受訊息頁面的window的引用。可以是頁面中iframe的contentWindow屬性;window.open的返回值;通過name或下標從window.frames取到的值。
message:所要傳送的資料,string型別。
targetOrigin:用於限制otherWindow,*表示不做限制。
eg1:
<iframe id="ifr" src="child.com/index.html"></iframe>
<script type="text/javascript">
window.onload = function() {
var ifr = document.getElementById('ifr');
var targetOrigin = 'http://child.com';
// 若寫成'http://child.com/c/proxy.html'效果一樣
// 若寫成'http://c.com'就不會執行postMessage了
ifr.contentWindow.postMessage('I was there!', targetOrigin);
};
</script>
<script type="text/javascript">
window.addEventListener('message', function(event){
// 通過origin屬性判斷訊息來源地址
if (event.origin == 'http://parent.com') {
alert(event.data); // 彈出"I was there!"
alert(event.source);
// 對parent.com、index.html中window物件的引用
// 但由於同源策略,這裡event.source不可以訪問window物件
}
}, false);
</script>
eg2:
假設在a.html裡嵌套個
<iframe src="http://www.child.com/b.html" frameborder="0"></iframe>
在這兩個頁面裡互相通訊
a.html
window.onload = function() {
window.addEventListener("message", function(e) {
alert(e.data);
});
window.frames[0].postMessage("b data", "http://www.child.com/b.html");
}
b.html
window.onload = function() {
window.addEventListener("message", function(e) {
alert(e.data);
});
window.parent.postMessage("a data", "http://www.parent.com/a.html");
}
這樣開啟a頁面,首先監聽到了b.html通過postMessage傳來的訊息,就先彈出 a data,然後a通過postMessage傳遞訊息給子頁面b.html,這時會彈出 b data
二、解決第3點限制:
AJAX請求不能傳送
4. 通過JSONP跨域
適用範圍:
- 可以是兩個完全不同源的域;
- 只支援HTTP請求中的GET方式;
- 老式瀏覽器全部支援;
- 需要服務端支援
JSONP(JSON with Padding)是資料格式JSON的一種使用模式,可以讓網頁從別的網域要資料。
由於瀏覽器的同源策略,在網頁端出現了這個“跨域”的問題,然而我們發現,所有的 src 屬性並沒有受到相關的限制,比如 img / script 等。
JSONP 的原理就要從 script 說起。script 可以引用其他域的指令碼檔案,比如這樣:
a.html
...
<script>
function callback(data) {
console.log(data.url)
}
</script>
<script src='b.js'></script>
...
b.js
callback({url: 'http://www.rccoder.net'})
這就類似於JSONP的原理了。
JSONP的基本思想是:先在網頁上新增一個script標籤,設定這個script標籤的src屬性用於向伺服器請求JSON資料 ,需要注意的是,src屬性的查詢字串一定要加一個callback引數,用來指定回撥函式的名字 。而這個函式是在資源載入之前就已經在前端定義好的,這個函式接受一個引數並利用這個引數做一些事情。向伺服器請求後,伺服器會將JSON資料放在一個指定名字的回撥函式裡作為其引數傳回來。這時,因為函式已經在前端定義好了,所以會直接呼叫。
eg:
function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.src = src;
document.body.appendChild(script);
}
window.onload = function () {
addScriptTag('http://example.com/ip?callback=foo');//請求伺服器資料並規定回撥函式為foo
}
function foo(data) {
console.log('Your public IP address is: ' + data.ip);
};
向伺服器example.com請求資料,這時伺服器會先生成JSON資料,這裡是{“ip”: “8.8.8.8”},然後以JS語法的方式生成一個函式,函式名就是傳遞上來的callback引數的值,最後將資料放在函式的引數中返回:
foo({
"ip": "8.8.8.8"
});
客戶端解析script標籤,執行返回的JS程式碼,呼叫函式。
5. 通過CORS跨域
適用範圍:
- 可以是兩個完全不同源的域;
- 支援所有型別的HTTP請求;
- 被絕大多數現代瀏覽器支援,老式瀏覽器不支援;
- 需要服務端支援
對於前端開發者來說,跨域的CORS通訊與同源的AJAX通訊沒有差別,程式碼完全一樣。因此,實現CORS通訊的關鍵是伺服器。只要伺服器實現了CORS介面,就可以跨源通訊。
瀏覽器將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
凡是不同時滿足上面兩個條件,就屬於非簡單請求。
瀏覽器對這兩種請求的處理,是不一樣的。
簡單請求:
下面是一次跨源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指定的源在許可範圍內,那麼伺服器返回的頭資訊中會包含Access-Control-Allow-Origin欄位,值與origin的值相同,以及其他幾個相關欄位:
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Access-Control-Allow-Origin: 該欄位是必須的。要麼與origin相同,要麼為*
Access-Control-Allow-Credentials: 該欄位可選。設為true表示伺服器允許傳送cookie
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欄位的值。
想要傳送cookie,這裡還有兩點需要額外注意:
1)開發者必須在AJAX請求中開啟withCredentials屬性。
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
否則即使伺服器允許,客戶端也不會發送。
2)Access-Control-Allow-Origin不能設為星號,必須指定明確的、與請求網頁一致的域名。同時,Cookie依然遵循同源政策,只有用伺服器域名設定的Cookie才會上傳,其他域名的Cookie並不會上傳,且(跨源)原網頁程式碼中的document.cookie也無法讀取伺服器域名下的Cookie。
非簡單請求:
1.預檢請求:
非簡單請求會在正式通訊前加一次預檢(preflight)請求。作用是瀏覽器先詢問伺服器當前網頁所在域名是否在伺服器的許可名單中,以及可以使用哪些HTTP方法以及頭資訊欄位。只有得到肯定答覆,瀏覽器才會傳送XMLHttpRequest,否則報錯。
一個例子:
var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
HTTP請求方法為PUT,併發送一個自定義頭資訊"X-Custom-Header",瀏覽器發現這是一個非簡單請求,就會自動傳送一個預檢請求,預檢請求的HTTP頭資訊如下:
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
請求方法是OPTIONS,表示這個請求是用來詢問的,頭資訊中的關鍵資訊有3個:
(1)表示請求來自哪個源
Origin: http://api.bob.com
(2)列出瀏覽器的CORS請求會用到哪些HTTP方法
Access-Control-Request-Method: PUT
(3)指定瀏覽器CORS請求會額外發送的頭資訊欄位
Access-Control-Request-Headers: X-Custom-Header
2.預檢請求的迴應(有兩種情況:A允許、B不允許)
A.伺服器允許這次跨域請求
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
伺服器返回中要注意的欄位:
(1)伺服器同意的跨域請求源:
Access-Control-Allow-Origin: http://api.bob.com
(2)伺服器支援的所有跨域請求的方法:
Access-Control-Allow-Methods: GET, POST, PUT
(3)表明伺服器支援的所有頭資訊欄位:
Access-Control-Allow-Headers: X-Custom-Header
(4)指定本次預檢請求的有效期,單位為秒,即允許請求該條迴應在有效期之前都不用再發送預檢請求:
Access-Control-Max-Age: 1728000
B.伺服器不允許這次跨域請求
即origin指定的源不在許可範圍內,伺服器會返回一個正常的HTTP迴應。但是頭資訊中沒有包含Access-Control-Allow-Origin欄位,就知道出錯了,從而丟擲一個錯誤,被XMLHttpRequest的onerror回撥函式捕獲。但是要注意的是,這種HTTP迴應的狀態碼很有可能是200,所以無法通過狀態碼識別這種錯誤。
3.正式請求
過了預檢請求,非簡單請求的正式請求就與簡單請求一樣了。