“同源政策”是瀏覽器安全的基石,其設計目的是為了保證資訊保安,防止惡意的網站竊取資料。所謂“同源”必須滿足以下三個方面:
- 協議相同
- 域名相同
- 埠相同(預設埠是80,可以省略)
如果是非同源的,以下行為會受到限制:
Cookie、LocalStorage
和IndexDB
無法讀取DOM
無法獲取AJAX
請求不能傳送
接下來我們主要講解如何解決以上三個方面的問題。
一、Cookie
Cookie
只有同源的網站才能獲取,但是如果兩個網頁的一級域名相同,只是二級域名不同,可以設定相同的document.domain
,兩個網頁就可以共享cookie
了。
很多人都誤把帶
www
當成一級域名,把其他字首的當成二級域名,是錯誤的。正確的域名劃分為:
- 頂級域名:
.com
- 一級域名:
baidu.com
- 二級域名:
tieba.baidu.com
舉例來說,A網頁是
http://w1.sillywa.com/a.html
,B網頁是http://w2.sillywa.com/b.html
,我們可以設定
document.domain = 'sillywa.com'
複製程式碼
這樣兩個網頁就可以共享Cookie
了。
注意,這種方法只是用於Cookie
和iframe
,LocalStorage
和IndexDB
無法通過這種方法規避同源政策,而是要是用PostMessage API
,下面我們會介紹。
二、iframe
如果兩個網頁不同源,就沒法拿到對方的DOM
。典型的例子是iframe
視窗和用window.open
方法開啟的視窗,它們與父視窗無法通訊。
所以對於完全不同源的網站,目前可以使用一下三種辦法規避同源問題:
- 片段識別符號(
fragment identifier
) window.name
- 跨文件通訊
API
(window.postMessage
)
1.片段識別符號
片段識別符號指的是URL
中#
後面的內容,比如http://sillywa.com/a.html#fragment
中的#fragment
,如果只是改變片段識別符號,頁面不會重新重新整理。
父視窗可以把資訊寫入子視窗的片段識別符號:
var src = originURL + '#' + data
document.getElementById('myIframe').src = src
複製程式碼
子視窗通過監聽hashchange
事件得到通知:
window.onhashchange = function() {
console.log(window.location.hash)
}
複製程式碼
2.window.name
瀏覽器視窗有window.name
屬性。這個屬性的最大特點是,無論是否同源,只要在同一個視窗裡,前一個網頁設定了這個屬性,後一個網頁可以讀取它。
3. window.postMessage
HTML5
為了解決跨視窗通訊問題引入了一個新的API
:跨文件通訊API
。這個API
為window
新增了一個window.postMessage()
方法,允許跨視窗通訊,不論這兩個視窗是否同源。舉例來說:假設父視窗為:http://aaa.com
,子視窗為:http://bbb.com
// 父視窗向子視窗傳送訊息
var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://bbb.com');
複製程式碼
postMessage()
方法的第一個引數是具體的資訊內容,第二個引數是接收訊息的視窗的源(origin
),即"協議 + 域名 + 埠"。也可以設為*,表示不限制域名,向所有視窗傳送。
同樣,子視窗向父視窗傳送訊息可以這樣寫:
window.opener.postMessage('Nice to see you', 'http://aaa.com');
複製程式碼
父視窗和子視窗都可以通過message
事件,監聽對方的訊息:
window.addEventListener('message', function(e) {
console.log(e.data)
},false)
複製程式碼
message
事件的event
物件有以下三個屬性:
event.source:
傳送訊息的視窗event.origin:
訊息傳送的網址event.data:
訊息內容
下面的例子是,子視窗通過event.source
屬性引用父視窗,然後傳送訊息。
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
event.source.postMessage('Nice to see you!', '*');
}
複製程式碼
如果我們將傳送的訊息改為LocalStorage
,則可以互相讀取LocalStorage
。
三、AJAX
同樣AJAX
請求也會受到同源策略的影響,除了使用代理伺服器外,還有一下方法可以實現跨域:
jsonp
WebScoket
CORS
1.jsonp
jsonp
想必大家都很瞭解,其由兩部分組成:回撥函式和資料。其基本思路是:動態插入script
標籤,向伺服器請求json
資料,返回的資料將在回撥函式裡獲得。
function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.src = src;
document.body.appendChild(script);
}
// 定義回撥函式
function foo(data) {
console.log('Your public IP address is: ' + data.ip);
};
window.onload = function () {
addScriptTag('http://example.com/ip?callback=foo');
}
複製程式碼
上面程式碼通過動態新增<script>
元素,向伺服器example.com
發出請求。注意,該請求的查詢字串有一個callback
引數,用來指定回撥函式的名字,這對於JSONP
是必需的。
2.WebScoket
WebScoket
不同於http
,它提供一種雙向通訊的功能,即客戶端可以向伺服器請求資料,同時伺服器也可以向客戶端傳送資料。而http
只能是單向的。
同時WebScoket
使用ws:\//
(非加密)和wss:\//
(加密)作為協議字首。該協議不實行同源政策,只要伺服器支援,就可以通過它進行跨源通訊。
要建立WebScoket
,先例項化一個WebScoket
物件並傳入要連線的URL
:
var scoket = new WebScoket("ws://www.example.com/server.php")
複製程式碼
例項化WebScoket
物件之後,瀏覽器會馬上嘗試建立連線。與XHR
類似,WebScoket
也有一系列表示當前狀態的readyState
屬性,如下:
WebScoket.OPENING
(0):正在建立連線WebScoket.OPEN
(1):已經建立連線WebScoket.CLOSING
(2):正在關閉連線WebScoket.ClOSE
(3):已經關閉連線
WebScoket
沒有readyStatechange
事件;不過它有其他的事件,我們待會介紹。
要關閉WebScoket
連線,可以呼叫close()
方法:
scoket.close()
複製程式碼
WebScoket
連線之後,就可以傳送和就收資料。要傳送資料可以呼叫send()
方法,並傳入字串,例如:
var scoket = new WebScoket("ws://www.example.com/server.php")
scoket.send('hello word')
複製程式碼
因為WebScoket
只能傳送純文字資料,所以對於複雜的資料型別我們應先將其序列化轉化為json字串
var message = {
name: 'sillywa'
}
scoket.send(JSON.stringify(message))
複製程式碼
同樣伺服器必須先解析再讀取資料。
當伺服器向客戶端發來訊息時,WebScoket
物件就會觸發message
事件。這個message
事件與其它傳遞訊息的協議類似,也就是把返回的資料儲存在event.data
的屬性中。
scoket.onmessage = function(event) {
console.log(event.data)
}
複製程式碼
與通過send()
傳送到伺服器的資料一樣,event.data
中返回的資料也是字串。
WebScoket
物件還有其他三個事件,在連線生命週期的不同階段觸發。
open:
在成功建立連線時觸發error
:在發生錯誤時觸發,連線不能持續close:
在連線關閉時觸發WebScoket
物件不支援DOM2
級事件偵聽器,因此必須使用DOM0
級語法分別定義每個事件處理程式。
var scoket = new WebScoket("ws://www.example.com/server.php")
scoket.onopen = function() {
console.log('connection start')
}
scoket.onerror = function() {
console.log('connection error')
}
scoket.onclose = function(event) {
console.log(event)
}
複製程式碼
在這三個事件中只有close
的event
物件有額外的資訊。這個事件的物件有三個額外的屬性:wasClean、code、reason
。其中wasClean
是一個布林值,表示連線是否已經明確地關閉;code
是伺服器返回的數值狀態碼;reason
是一個字串,包含伺服器發回的資訊。
3.CORS
CORS
是一個W3C
標準,全稱是"跨域資源共享"(Cross-origin resource sharing
)。
它允許瀏覽器向跨源伺服器,發出XMLHttpRequest
請求,從而克服了AJAX
只能同源使用的限制。
相比jsonp
只能傳送get
請求,CORS
允許傳送任何型別的請求。但CORS
要求瀏覽器和伺服器同時支援。目前所有瀏覽器都支援,IE
需要IE10
以上。
整個CORS
通訊過程中都是瀏覽器自動完成,不需要使用者的參與。CORS
通訊和同源的AJAX
請求沒有區別。瀏覽器一旦發現AJAX
請求跨域,就會自動新增一些頭部資訊,有時候還會多出一次附加請求。
瀏覽器將CORS
請求分為兩類:簡單請求和非簡單請求。
只要同時滿足一下兩個條件就是簡單請求,否則就是非簡單請求:
(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
對於簡單請求,瀏覽器會自動在頭部資訊裡增加一個Origin
欄位,用來表示請求來自與哪個源,伺服器根據這個值決定是否同意此次請求。如果Origin
不在請求範圍內,伺服器返回一個正常的http
回應。這個回應的頭資訊中沒有Access-Control-Allow-Origin
欄位,瀏覽器發現沒有這個欄位之後就會丟擲一個錯誤。如果Origin
在請求範圍內,伺服器返回的響應會多出幾個頭資訊欄位,其中一個是Access-Control-Allow-Origin
,它的值要麼是Origin
的值,要麼是*,表示允許任何域名的請求。
對於非簡單請求,它會在正式通訊之前,增加一次http
查詢請求,稱為"預檢"請求(preflight
)。通常是一個OPTION
請求。這個請求先詢問伺服器,當前網頁所在的域名是否在伺服器的許可名單之中,以及可以使用哪http
動詞和頭資訊欄位。只有得到肯定答覆,瀏覽器才會發出正式的XMLHttpRequest
請求,否則就報錯。
如果大家想要更詳細的瞭解CORS
,可以參考以下文章。
參考文章:
阮一峰《瀏覽器同源政策及其規避方法》
阮一峰《跨域資源共享 CORS 詳解》
參考書籍:
《javascript高階程式設計》