同源策略及其解決方案

Sillywa發表於2018-03-20

“同源政策”是瀏覽器安全的基石,其設計目的是為了保證資訊保安,防止惡意的網站竊取資料。所謂“同源”必須滿足以下三個方面:

  1. 協議相同
  2. 域名相同
  3. 埠相同(預設埠是80,可以省略)

如果是非同源的,以下行為會受到限制:

  • Cookie、LocalStorageIndexDB無法讀取
  • DOM無法獲取
  • AJAX請求不能傳送

接下來我們主要講解如何解決以上三個方面的問題。

一、Cookie

Cookie只有同源的網站才能獲取,但是如果兩個網頁的一級域名相同,只是二級域名不同,可以設定相同的document.domain,兩個網頁就可以共享cookie了。

很多人都誤把帶www當成一級域名,把其他字首的當成二級域名,是錯誤的。正確的域名劃分為:

  1. 頂級域名:.com
  2. 一級域名:baidu.com
  3. 二級域名:tieba.baidu.com

舉例來說,A網頁是http://w1.sillywa.com/a.html,B網頁是http://w2.sillywa.com/b.html,我們可以設定

document.domain = 'sillywa.com'
複製程式碼

這樣兩個網頁就可以共享Cookie了。

注意,這種方法只是用於CookieiframeLocalStorageIndexDB無法通過這種方法規避同源政策,而是要是用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。這個APIwindow新增了一個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物件有以下三個屬性:

  1. event.source:傳送訊息的視窗
  2. event.origin:訊息傳送的網址
  3. 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)
}
複製程式碼

在這三個事件中只有closeevent物件有額外的資訊。這個事件的物件有三個額外的屬性: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高階程式設計》

相關文章