同源策略 Same Origin Policy
日常開發中最常與網路打交道,那關於瀏覽器的同源策略和跨域相關的知識是該整理一下了。
首先需要明確的是,同源策略是瀏覽器的安全策略,由於存在這個策略,我們才需要對各種跨域需求進行處理。
同源策略的主要目的是為了保護使用者的資訊保安。
什麼是同源
同源的含義其實比較好理解,實際上就是三點
- 協議相同
- 域名相同
- 埠相同
url | 說明 | 是否同源 |
---|---|---|
http://www.test.com https://www.test.com | 同一域名,不同協議 | 不同源 |
http://www.test.com https://www.test1.com | 不同域名 | 不同源 |
http://www.test.com:8080 https://www.test.com:8081 | 同一域名,不同埠 | 不同源 |
https://www.test.com http://192.168.1.1 | 域名和域名對應的 ip | 不同源 |
同源限制了什麼
1、Cookie、localStorage 和 IndexDB 無法讀取
2、DOM 無法獲得
3、AJAX 請求不能傳送
規避方法
1、讀取 Cookie
cookie 的讀取與其他屬性有些不同。因為 cookie 的可見性是由 domain 屬性和 path 屬性決定的,所以他其實不受協議和埠的限制。只要是同一個 domain 和可見 path 下的 cookie,都能讀取到。
可見 path
- / 只能讀取 / 下的 cookie
- /test 可以讀取 / 和 /test 下的 cookie
由於 cookie 的讀取本身具有侷限性,所以跨域後的規避可以這樣操作
- 前端讀取不同 domain 的 cookie,可以把通過設定 document.domain 來讀取相應 cookie。當然,你設定的 domain 必須是你當前 domain 的上一級域名,否則會報如下的錯誤。
Uncaught DOMException: Failed to set the 'domain' property on 'Document': 'test' is not a suffix of 'localhost'.
- 前端和伺服器端設定 cookie 時指定可讀的 domian 和 path,這個 domain 也遵循上面的規則,只能設定當前 domain 或 domain 上一級的域名,否則會設定失敗。本地測試的現象是沒有報錯,但是也沒有設定成功。
2、讀取 localStorage、IndexedDB 和 DOM
localStorage、IndexedDB 和 DOM 是實實在在受同源策略限制的,協議、域名和埠任意一項不相同就無法讀取。
其實對於他們的讀取可以簡化成不同源的兩個頁面如何通訊。
本身 localStorage 和 IndexedDB 的出現就是為了讓我們能夠更好地儲存資料,而獲取 DOM 大多情況也是為了獲取 DOM 中的各種資料。泛化到業務場景中,也就是傳遞資料的事兒了。
目前可以通過以下幾種方式來進行通訊。
-
window.name
使用 window.name 的原理是同源 iframe 可以讀取 contentWindow。具體操作如下
1、a 頁面先載入一個不同源的 iframe 頁面 b
2、在 b 頁面 中修改 window.name 為需要的傳遞的資料
3、a 頁面中修改這個 iframe 的 src 為同源的頁面 c
4、a 頁面獲取之前設定的 window.name
按照這樣的方式進行操作就能獲取到子頁面中的資料。如果子頁面想要獲取父頁面中的資料,可以將 1、3 步驟換一下,2 步驟改成父頁面直接修改 iframe.contentWindow.name 為需要傳遞的資料即可。
因為是通過 name 屬性來傳遞引數,所以可傳遞的資料量很大,基本就是字串長度的最大值。//記錄 iframe onload 事件的載入次數 var state = 0; var iframe = document.createElement("iframe"); // 載入跨域頁面 iframe.src = "http://sub.test.com/test/test.html"; // onload 事件會觸發2次,第1次載入跨域頁,在跨域頁中將所需要的資料賦給 window.name iframe.onload = function () { if (state === 1) { // 第2次 onload 成功後,讀取同域 window.name 中資料 var data = iframe.contentWindow.name; } else if (state === 0) { // 第1次onload(跨域頁)成功後,切換到同域代理頁面 iframe.contentWindow.location = "./testB.html"; state = 1; } }; document.body.appendChild(iframe);
-
fragment identifier 片段識別符號
fragment identifier 其實就是 url # 後面的部分。常寫 SPA 應用的小夥伴應該會對他很熟悉,因為 hash 模式的 router 就是基於他實現的。修改嵌入的 iframe 的 src 為 url#需要傳遞的資料,iframe 頁面就能通過監聽 hashchange 事件獲得傳遞的資料。如果是子頁面向父頁面傳遞資料要多加一步,得先讓父頁面把自己當前的 url 傳遞給子頁面,然後子頁面去修改父頁面的 href。
使用這個方法傳遞資料的限制暫時還未測試,猜測應該會和瀏覽器限制 url 的長度有關。//父->子 a頁面 var iframe = document.createElement("iframe"); // 載入跨域頁面 iframe.src = url; setTimeout(function () { iframe.src = url + "#a=111"; }, 1000); document.body.appendChild(iframe); //父->子 b頁面 window.onhashchange = function () { alert(window.location.hash) }
//子->父 a頁面 var iframe = document.createElement("iframe"); // 載入跨域頁面 iframe.src = url; setTimeout(function () { var selfurl = document.location.href; iframe.src = url + "#" + selfurl; }, 1000); document.body.appendChild(iframe); window.onhashchange = function () { alert("from son" + window.location.hash); }; //子->父 b頁面 window.onhashchange = function () { let target = window.location.hash.slice(1); window.parent.parent.location.href = target + "#111aaaa" }
-
postMessage
嚴格意義上來說上面兩種方法都是對跨域頁面獲取資料的破解,postMessage 才是正統的非同源頁面之間傳遞資料的方法。window.postMessage() 方法提供了一種受控機制來規避此限制,只要正確的使用,這種方法就很安全。
postMessage 是新增的 API,使用前記得檢視一下相容性。
使用上需要注意的就是呼叫 postMessage 和監聽 message 事件的的主體是同一個。也就是說子頁面向父頁面傳送訊息時,需要獲取 window.parent 去呼叫 postMessage。父頁面向子頁面傳送訊息時,可以直接獲取 iframe,然後呼叫 postMessage。postMessage((message, targetOrigin, [transfer]);
message
將要傳送到其他 window 的資料。它將會被結構化克隆演算法序列化。這意味著你可以不受什麼限制的將資料物件安全的傳送給目標視窗而無需自己序列化。
targetOrigin
通過視窗的 origin 屬性來指定哪些視窗能接收到訊息事件,其值可以是字串""(表示無限制)或者一個 URI。
在傳送訊息的時候,如果目標視窗的協議、主機地址或埠這三者的任意一項不匹配 targetOrigin 提供的值,那麼訊息就不會被髮送;只有三者完全匹配,訊息才會被髮送。
這個機制用來控制訊息可以傳送到哪些視窗;例如,當用 postMessage 傳送密碼時,這個引數就顯得尤為重要,必須保證它的值與這條包含密碼的資訊的預期接受者的 origin 屬性完全一致,來防止密碼被惡意的第三方截獲。
如果你明確的知道訊息應該傳送到哪個視窗,那麼請始終提供一個有確切值的 targetOrigin,而不是。不提供確切的目標將導致資料洩露到任何對資料感興趣的惡意站點。
transfer 可選
是一串和message 同時傳遞的 Transferable 物件. 這些物件的所有權將被轉移給訊息的接收方,而傳送一方將不再保有所有權下面這個示例是子頁面向父頁面傳送訊息。
// a 頁面 window.addEventListener("message", function (event) { console.log(event); }, false); // b 頁面 var b = "bbb"; window.parent.postMessage({ b }, "*")
-
另外特別需要注意的是關於 DOM 的獲取,如果只是兩個不同子域的頁面,將 document.domain 設定為同一主域就可以讀取相應資料。
//a 頁面 main.test.com var a = "aaa"; // domain 設定為主域 document.domain = "test.com"; var iframe = document.createElement("iframe"); // 載入跨域頁面 iframe.src = "sub.test.com"; document.body.appendChild(iframe); iframe.onload = function(){ // 可以獲取到變數 var data = iframe.contentWindow.b // 可以獲取到 DOM var dom = iframe.contentWindow.document.body; } //b 頁面 sub.test.com var b = "bbb"; // domain 設定為主域 document.domain = "test.com";
3、AJAX 請求
AJAX 請求跨域日常使用比較多,常用的方法有以下幾種
-
JSONP
這個方法是向伺服器請求的時候,在 url 後面寫上 callback 方法的名字,請求返回實際上是返回了一個 呼叫 callback 方法的 js 檔案。需要返回的引數也就在呼叫的時候傳進去了。
所以侷限也很明確,只支援 get 方法。 -
使用代理伺服器
所有的請求先傳送給這個代理伺服器,由這個代理伺服器去請求實際的介面,再把需要的資料返回。
(這個方式就可以真切地體會到,同源策略只是瀏覽器的一種安全策略) -
使用 CORS Cross-Origin Resource Sharing 跨域資源共享
目前基本上所有的瀏覽器都支援 CORS,所以只需要伺服器端進行處理。對於前端來說,請求和同源的 AJAX 請求是一致的。
前端傳送請求的時候瀏覽器會自動帶上 origin 欄位,伺服器端去判斷這個 origin 是否是可接受的地址,在相應頭中設定 Access-Control-Allow-Origin 欄位的值。
這樣前端就能正常獲取到資料啦。
這裡只對 CORS 做了一個簡單介紹,詳細的下次細說吧。
總結:
- 讀取跨域的 cookie 需要設定 path 和 domain
- 不同源頁面間通訊可以通過設定 window.name、location 的 hash 值和 postMessage 來實現。
不難發現的是,設定 hash 和 postMessage 都是通過設定一個監聽函式來實現的,所以我們是非同步獲取資料的。
而設定 window.name 雖然是在 iframe.onload 事件中獲取的,但是本質上是在等待 iframe 的載入,確保資料已經設定成功。
這裡還有值得思考的地方。
參考資料
瀏覽器同源政策及其規避方法
瀏覽器的同源策略
window.postMessage
跨源資源共享(CORS)
前端常見跨域解決方案(全)