本文詳細介紹"同源政策"的各個方面,以及如何規避它。

1995年,同源政策由 netscape 公司引入浏覽器。目前,所有浏覽器都實行這個政策。
最初,它的含義是指,a網頁設定的 cookie,b網頁不能打開,除非這兩個網頁"同源"。所謂"同源"指的是"三個相同"。
協定相同 域名相同 端口相同
舉例來說,<code>http://www.example.com/dir/page.html</code>這個網址,協定是<code>http://</code>,域名是<code>www.example.com</code>,端口是<code>80</code>(預設端口可以省略)。它的同源情況如下。
<code>http://www.example.com/dir2/other.html</code>:同源 <code>http://example.com/dir/other.html</code>:不同源(域名不同) <code>http://v2.www.example.com/dir/other.html</code>:不同源(域名不同) <code>http://www.example.com:81/dir/other.html</code>:不同源(端口不同)
同源政策的目的,是為了保證使用者資訊的安全,防止惡意的網站竊取資料。
設想這樣一種情況:a網站是一家銀行,使用者登入以後,又去浏覽其他網站。如果其他網站可以讀取a網站的 cookie,會發生什麼?
很顯然,如果 cookie 包含隐私(比如存款總額),這些資訊就會洩漏。更可怕的是,cookie 往往用來儲存使用者的登入狀态,如果使用者沒有登出,其他網站就可以冒充使用者,為所欲為。因為浏覽器同時還規定,送出表單不受同源政策的限制。
由此可見,"同源政策"是必需的,否則 cookie 可以共享,網際網路就毫無安全可言了。
随着網際網路的發展,"同源政策"越來越嚴格。目前,如果非同源,共有三種行為受到限制。
(1) cookie、localstorage 和 indexdb 無法讀取。 (2) dom 無法獲得。 (3) ajax 請求不能發送。
雖然這些限制是必要的,但是有時很不友善,合理的用途也受到影響。下面,我将詳細介紹,如何規避上面三種限制。
cookie 是伺服器寫入浏覽器的一小段資訊,隻有同源的網頁才能共享。但是,兩個網頁一級域名相同,隻是二級域名不同,浏覽器允許通過設定<code>document.domain</code>共享 cookie。
舉例來說,a網頁是<code>http://w1.example.com/a.html</code>,b網頁是<code>http://w2.example.com/b.html</code>,那麼隻要設定相同的<code>document.domain</code>,兩個網頁就可以共享cookie。
現在,a網頁通過腳本設定一個 cookie。
b網頁就可以讀到這個 cookie。
注意,這種方法隻适用于 cookie 和 iframe 視窗,localstorage 和 indexdb 無法通過這種方法,規避同源政策,而要使用下文介紹的postmessage api。
另外,伺服器也可以在設定cookie的時候,指定cookie的所屬域名為一級域名,比如<code>.example.com</code>。
這樣的話,二級域名和三級域名不用做任何設定,都可以讀取這個cookie。
如果兩個網頁不同源,就無法拿到對方的dom。典型的例子是<code>iframe</code>視窗和<code>window.open</code>方法打開的視窗,它們與父視窗無法通信。
比如,父視窗運作下面的指令,如果<code>iframe</code>視窗不是同源,就會報錯。
上面指令中,父視窗想擷取子視窗的dom,因為跨源導緻報錯。
反之亦然,子視窗擷取主視窗的dom也會報錯。
如果兩個視窗一級域名相同,隻是二級域名不同,那麼設定上一節介紹的<code>document.domain</code>屬性,就可以規避同源政策,拿到dom。
對于完全不同源的網站,目前有三種方法,可以解決跨域視窗的通信問題。
片段識别符(fragment identifier) window.name 跨文檔通信api(cross-document messaging)
片段辨別符(fragment identifier)指的是,url的<code>#</code>号後面的部分,比如<code>http://example.com/x.html#fragment</code>的<code>#fragment</code>。如果隻是改變片段辨別符,頁面不會重新重新整理。
父視窗可以把資訊,寫入子視窗的片段辨別符。
子視窗通過監聽<code>hashchange</code>事件得到通知。
同樣的,子視窗也可以改變父視窗的片段辨別符。
浏覽器視窗有<code>window.name</code>屬性。這個屬性的最大特點是,無論是否同源,隻要在同一個視窗裡,前一個網頁設定了這個屬性,後一個網頁可以讀取它。
父視窗先打開一個子視窗,載入一個不同源的網頁,該網頁将資訊寫入<code>window.name</code>屬性。
接着,子視窗跳回一個與主視窗同域的網址。
然後,主視窗就可以讀取子視窗的<code>window.name</code>了。
這種方法的優點是,<code>window.name</code>容量很大,可以放置非常長的字元串;缺點是必須監聽子視窗<code>window.name</code>屬性的變化,影響網頁性能。
上面兩種方法都屬于破解,html5為了解決這個問題,引入了一個全新的api:跨文檔通信 api(cross-document messaging)。
這個api為<code>window</code>對象新增了一個<code>window.postmessage</code>方法,允許跨視窗通信,不論這兩個視窗是否同源。
舉例來說,父視窗<code>http://aaa.com</code>向子視窗<code>http://bbb.com</code>發消息,調用<code>postmessage</code>方法就可以了。
<code>postmessage</code>方法的第一個參數是具體的資訊内容,第二個參數是接收消息的視窗的源(origin),即"協定 + 域名 + 端口"。也可以設為<code>*</code>,表示不限制域名,向所有視窗發送。
子視窗向父視窗發送消息的寫法類似。
父視窗和子視窗都可以通過<code>message</code>事件,監聽對方的消息。
<code>message</code>事件的事件對象<code>event</code>,提供以下三個屬性。
<code>event.source</code>:發送消息的視窗 <code>event.origin</code>: 消息發向的網址 <code>event.data</code>: 消息内容
下面的例子是,子視窗通過<code>event.source</code>屬性引用父視窗,然後發送消息。
<code>event.origin</code>屬性可以過濾不是發給本視窗的消息。
通過<code>window.postmessage</code>,讀寫其他視窗的 localstorage 也成為了可能。
下面是一個例子,主視窗寫入iframe子視窗的<code>localstorage</code>。
上面代碼中,子視窗将父視窗發來的消息,寫入自己的localstorage。
父視窗發送消息的代碼如下。
加強版的子視窗接收消息的代碼如下。
加強版的父視窗發送消息代碼如下。
同源政策規定,ajax請求隻能發給同源的網址,否則就報錯。
除了架設伺服器代理(浏覽器請求同源伺服器,再由後者請求外部服務),有三種方法規避這個限制。
jsonp websocket cors
jsonp是伺服器與用戶端跨源通信的常用方法。最大特點就是簡單适用,老式浏覽器全部支援,伺服器改造非常小。
它的基本思想是,網頁通過添加一個<code><script></code>元素,向伺服器請求json資料,這種做法不受同源政策限制;伺服器收到請求後,将資料放在一個指定名字的回調函數裡傳回來。
首先,網頁動态插入<code><script></code>元素,由它向跨源網址送出請求。
上面代碼通過動态添加<code><script></code>元素,向伺服器<code>example.com</code>送出請求。注意,該請求的查詢字元串有一個<code>callback</code>參數,用來指定回調函數的名字,這對于jsonp是必需的。
伺服器收到這個請求以後,會将資料放在回調函數的參數位置傳回。
由于<code><script></code>元素請求的腳本,直接作為代碼運作。這時,隻要浏覽器定義了<code>foo</code>函數,該函數就會立即調用。作為參數的json資料被視為javascript對象,而不是字元串,是以避免了使用<code>json.parse</code>的步驟。
websocket是一種通信協定,使用<code>ws://</code>(非加密)和<code>wss://</code>(加密)作為協定字首。該協定不實行同源政策,隻要伺服器支援,就可以通過它進行跨源通信。
上面代碼中,有一個字段是<code>origin</code>,表示該請求的請求源(origin),即發自哪個域名。
正是因為有了<code>origin</code>這個字段,是以websocket才沒有實行同源政策。因為伺服器可以根據這個字段,判斷是否許可本次通信。如果該域名在白名單内,伺服器就會做出如下回應。
cors是跨源資源分享(cross-origin resource sharing)的縮寫。它是w3c标準,是跨源ajax請求的根本解決方法。相比jsonp隻能發<code>get</code>請求,cors允許任何類型的請求。