什麼是瀏覽器同源策略 同源策略(Same origin policy)是一種約定,它是瀏覽器最核心也最基本的安全功能,如果缺少了同源策略,則瀏覽器的正常功能可能都會受到影響。可以說 Web 是構建在同源策略基礎之上的,瀏覽器只是針對同源策略的一種實現。 它的核心就在於它認為自任何站點裝載的信賴內容是 ...
什麼是瀏覽器同源策略
同源策略(Same origin policy)是一種約定,它是瀏覽器最核心也最基本的安全功能,如果缺少了同源策略,則瀏覽器的正常功能可能都會受到影響。可以說 Web 是構建在同源策略基礎之上的,瀏覽器只是針對同源策略的一種實現。
它的核心就在於它認為自任何站點裝載的信賴內容是不安全的。當被瀏覽器半信半疑的腳本運行在沙箱時,它們應該只被允許訪問來自同一站點的資源,而不是那些來自其它站點可能懷有惡意的資源。
所謂同源是指:功能變數名稱、協議、埠相同。
下表是相對於 http://www.laixiangran.cn/home/index.html
的同源檢測結果:
另外,同源策略又分為以下兩種:
- DOM 同源策略:禁止對不同源頁面 DOM 進行操作。這裡主要場景是 iframe 跨域的情況,不同功能變數名稱的 iframe 是限制互相訪問的。
- XMLHttpRequest 同源策略:禁止使用 XHR 對象向不同源的伺服器地址發起 HTTP 請求。
為什麼要有跨域限制
因為存在瀏覽器同源策略,所以才會有跨域問題。那麼瀏覽器是出於何種原因會有跨域的限制呢。其實不難想到,跨域限制主要的目的就是為了用戶的上網安全。
如果瀏覽器沒有同源策略,會存在什麼樣的安全問題呢。下麵從 DOM 同源策略和 XMLHttpRequest 同源策略來舉例說明:
如果沒有 DOM 同源策略,也就是說不同域的 iframe 之間可以相互訪問,那麼黑客可以這樣進行攻擊:
- 做一個假網站,裡面用 iframe 嵌套一個銀行網站
http://mybank.com
。 - 把 iframe 寬高啥的調整到頁面全部,這樣用戶進來除了功能變數名稱,別的部分和銀行的網站沒有任何差別。
- 這時如果用戶輸入賬號密碼,我們的主網站可以跨域訪問到
http://mybank.com
的 dom 節點,就可以拿到用戶的賬戶密碼了。
如果 XMLHttpRequest 同源策略,那麼黑客可以進行 CSRF(跨站請求偽造) 攻擊:
- 用戶登錄了自己的銀行頁面
http://mybank.com
,http://mybank.com
向用戶的 cookie 中添加用戶標識。 - 用戶瀏覽了惡意頁面
http://evil.com
,執行了頁面中的惡意 AJAX 請求代碼。 http://evil.com
向http://mybank.com
發起 AJAX HTTP 請求,請求會預設把http://mybank.com
對應 cookie 也同時發送過去。- 銀行頁面從發送的 cookie 中提取用戶標識,驗證用戶無誤,response 中返回請求數據。此時數據就泄露了。
- 而且由於 Ajax 在後臺執行,用戶無法感知這一過程。
因此,有了瀏覽器同源策略,我們才能更安全的上網。
跨域的解決方法
從上面我們瞭解到了瀏覽器同源策略的作用,也正是有了跨域限制,才使我們能安全的上網。但是在實際中,有時候我們需要突破這樣的限制,因此下麵將介紹幾種跨域的解決方法。
CORS(跨域資源共用)
CORS(Cross-origin resource sharing,跨域資源共用)是一個 W3C 標準,定義了在必須訪問跨域資源時,瀏覽器與伺服器應該如何溝通。CORS 背後的基本思想,就是使用自定義的 HTTP 頭部讓瀏覽器與伺服器進行溝通,從而決定請求或響應是應該成功,還是應該失敗。
CORS 需要瀏覽器和伺服器同時支持。目前,所有瀏覽器都支持該功能,IE 瀏覽器不能低於 IE10。
整個 CORS 通信過程,都是瀏覽器自動完成,不需要用戶參與。對於開發者來說,CORS 通信與同源的 AJAX 通信沒有差別,代碼完全一樣。瀏覽器一旦發現 AJAX 請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感覺。
因此,實現 CORS 通信的關鍵是伺服器。只要伺服器實現了 CORS 介面,就可以跨源通信。
瀏覽器將CORS請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。
只要同時滿足以下兩大條件,就屬於簡單請求。
- 請求方法是以下三種方法之一:
- HEAD
- GET
- POST
- HTTP的頭信息不超出以下幾種欄位:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限於三個值 application/x-www-form-urlencoded、multipart/form-data、text/plain
凡是不同時滿足上面兩個條件,就屬於非簡單請求。
瀏覽器對這兩種請求的處理,是不一樣的。
簡單請求
- 在請求中需要附加一個額外的 Origin 頭部,其中包含請求頁面的源信息(協議、功能變數名稱和埠),以便伺服器根據這個頭部信息來決定是否給予響應。例如:
Origin: http://www.laixiangran.cn
- 如果伺服器認為這個請求可以接受,就在 Access-Control-Allow-Origin 頭部中回發相同的源信息(如果是公共資源,可以回發 * )。例如:
Access-Control-Allow-Origin:http://www.laixiangran.cn
- 沒有這個頭部或者有這個頭部但源信息不匹配,瀏覽器就會駁回請求。正常情況下,瀏覽器會處理請求。註意,請求和響應都不包含 cookie 信息。
- 如果需要包含 cookie 信息,ajax 請求需要設置 xhr 的屬性 withCredentials 為 true,伺服器需要設置響應頭部
Access-Control-Allow-Credentials: true
。
非簡單請求
瀏覽器在發送真正的請求之前,會先發送一個 Preflight 請求給伺服器,這種請求使用 OPTIONS 方法,發送下列頭部:
- Origin:與簡單的請求相同。
- Access-Control-Request-Method: 請求自身使用的方法。
- Access-Control-Request-Headers: (可選)自定義的頭部信息,多個頭部以逗號分隔。
例如:
Origin: http://www.laixiangran.cn
Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ
發送這個請求後,伺服器可以決定是否允許這種類型的請求。伺服器通過在響應中發送如下頭部與瀏覽器進行溝通:
- Access-Control-Allow-Origin:與簡單的請求相同。
- Access-Control-Allow-Methods: 允許的方法,多個方法以逗號分隔。
- Access-Control-Allow-Headers: 允許的頭部,多個方法以逗號分隔。
- Access-Control-Max-Age: 應該將這個 Preflight 請求緩存多長時間(以秒錶示)。
例如:
Access-Control-Allow-Origin: http://www.laixiangran.cn
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: NCZ
Access-Control-Max-Age: 1728000
一旦伺服器通過 Preflight 請求允許該請求之後,以後每次瀏覽器正常的 CORS 請求,就都跟簡單請求一樣了。
優點
- CORS 通信與同源的 AJAX 通信沒有差別,代碼完全一樣,容易維護。
- 支持所有類型的 HTTP 請求。
缺點
- 存在相容性問題,特別是 IE10 以下的瀏覽器。
- 第一次發送非簡單請求時會多一次請求。
JSONP 跨域
由於 script
標簽不受瀏覽器同源策略的影響,允許跨域引用資源。因此可以通過動態創建 script 標簽,然後利用 src 屬性進行跨域,這也就是 JSONP 跨域的基本原理。
直接通過下麵的例子來說明 JSONP 實現跨域的流程:
// 1. 定義一個 回調函數 handleResponse 用來接收返回的數據
function handleResponse(data) {
console.log(data);
};
// 2. 動態創建一個 script 標簽,並且告訴後端回調函數名叫 handleResponse
var body = document.getElementsByTagName('body')[0];
var script = document.gerElement('script');
script.src = 'http://www.laixiangran.cn/json?callback=handleResponse';
body.appendChild(script);
// 3. 通過 script.src 請求 `http://www.laixiangran.cn/json?callback=handleResponse`,
// 4. 後端能夠識別這樣的 URL 格式並處理該請求,然後返回 handleResponse({"name": "laixiangran"}) 給瀏覽器
// 5. 瀏覽器在接收到 handleResponse({"name": "laixiangran"}) 之後立即執行 ,也就是執行 handleResponse 方法,獲得後端返回的數據,這樣就完成一次跨域請求了。
優點
- 使用簡便,沒有相容性問題,目前最流行的一種跨域方法。
缺點
- 只支持 GET 請求。
- 由於是從其它域中載入代碼執行,因此如果其他域不安全,很可能會在響應中夾帶一些惡意代碼。
- 要確定 JSONP 請求是否失敗並不容易。雖然 HTML5 給 script 標簽新增了一個 onerror 事件處理程式,但是存在相容性問題。
圖像 Ping 跨域
由於 img
標簽不受瀏覽器同源策略的影響,允許跨域引用資源。因此可以通過 img 標簽的 src 屬性進行跨域,這也就是圖像 Ping 跨域的基本原理。
直接通過下麵的例子來說明圖像 Ping 實現跨域的流程:
var img = new Image();
// 通過 onload 及 onerror 事件可以知道響應是什麼時候接收到的,但是不能獲取響應文本
img.onload = img.onerror = function() {
console.log("Done!");
}
// 請求數據通過查詢字元串形式發送
img.src = 'http://www.laixiangran.cn/test?name=laixiangran';
優點
- 用於實現跟蹤用戶點擊頁面或動態廣告曝光次數有較大的優勢。
缺點
- 只支持 GET 請求。
- 只能瀏覽器與伺服器的單向通信,因為瀏覽器不能訪問伺服器的響應文本。
伺服器代理
瀏覽器有跨域限制,但是伺服器不存在跨域問題,所以可以由伺服器請求所要域的資源再返回給客戶端。
伺服器代理是萬能的。
document.domain 跨域
對於主功能變數名稱相同,而子功能變數名稱不同的情況,可以使用 document.domain 來跨域。這種方式非常適用於 iframe 跨域的情況。
比如,有一個頁面,它的地址是 http://www.laixiangran.cn/a.html
,在這個頁面裡面有一個 iframe,它的 src 是 http://laixiangran.cn/b.html
。很顯然,這個頁面與它裡面的 iframe 框架是不同域的,所以我們是無法通過在頁面中書寫 js 代碼來獲取 iframe 中的東西的。
這個時候,document.domain 就可以派上用場了,我們只要把 http://www.laixiangran.cn/a.html
和 http://laixiangran.cn/b.html
這兩個頁面的 document.domain 都設成相同的功能變數名稱就可以了。但要註意的是,document.domain 的設置是有限制的,我們只能把 document.domain 設置成自身或更高一級的父域,且主域必須相同。例如:a.b.laixiangran.cn
中某個文檔的 document.domain 可以設成 a.b.laixiangran.cn
、b.laixiangran.cn
、laixiangran.cn
中的任意一個,但是不可以設成 c.a.b.laixiangran.cn
,因為這是當前域的子域,也不可以設成 baidu.com
,因為主域已經不相同了。
例如,在頁面 http://www.laixiangran.cn/a.html
中設置document.domain:
<iframe src="http://laixiangran.cn/b.html" id="myIframe" onload="test()">
<script>
document.domain = 'laixiangran.cn'; // 設置成主域
function test() {
console.log(document.getElementById('myIframe').contentWindow);
}
</script>
在頁面 http://laixiangran.cn/b.html
中也設置 document.domain,而且這也是必須的,雖然這個文檔的 domain 就是 laixiangran.cn
,但是還是必須顯式地設置 document.domain 的值:
<script>
document.domain = 'laixiangran.cn'; // document.domain 設置成與主頁面相同
</script>
這樣,http://www.laixiangran.cn/a.html
就可以通過 js 訪問到 http://laixiangran.cn/b.html
中的各種屬性和對象了。
window.name 跨域
window 對象有個 name 屬性,該屬性有個特征:即在一個視窗(window)的生命周期內,視窗載入的所有的頁面(不管是相同域的頁面還是不同域的頁面)都是共用一個 window.name
的,每個頁面對 window.name
都有讀寫的許可權,window.name
是持久存在一個視窗載入過的所有頁面中的,並不會因新頁面的載入而進行重置。
通過下麵的例子介紹如何通過 window.name 來跨域獲取數據的。
頁面 http://www.laixiangran.cn/a.html
的代碼:
<iframe src="http://laixiangran.cn/b.html" id="myIframe" onload="test()" style="display: none;">
<script>
// 2. iframe載入 "http://laixiangran.cn/b.html 頁面後會執行該函數
function test() {
var iframe = document.getElementById('myIframe');
// 重置 iframe 的 onload 事件程式,
// 此時經過後面代碼重置 src 之後,
// http://www.laixiangran.cn/a.html 頁面與該 iframe 在同一個源了,可以相互訪問了
iframe.onload = function() {
var data = iframe.contentWindow.name; // 4. 獲取 iframe 里的 window.name
console.log(data); // hello world!
};
// 3. 重置一個與 http://www.laixiangran.cn/a.html 頁面同源的頁面
iframe.src = 'http://www.laixiangran.cn/c.html';
}
</script>
頁面 http://laixiangran.cn/b.html
的代碼:
<script type="text/javascript">
// 1. 給當前的 window.name 設置一個 http://www.laixiangran.cn/a.html 頁面想要得到的數據值
window.name = "hello world!";
</script>
location.hash 跨域
location.hash 方式跨域,是子框架具有修改父框架 src 的 hash 值,通過這個屬性進行傳遞數據,且更改 hash 值,頁面不會刷新。但是傳遞的數據的位元組數是有限的。
頁面 http://www.laixiangran.cn/a.html
的代碼:
<iframe src="http://laixiangran.cn/b.html" id="myIframe" onload="test()" style="display: none;">
<script>
// 2. iframe載入 "http://laixiangran.cn/b.html 頁面後會執行該函數
function test() {
// 3. 獲取通過 http://laixiangran.cn/b.html 頁面設置 hash 值
var data = window.location.hash;
console.log(data);
}
</script>
頁面 http://laixiangran.cn/b.html
的代碼:
<script type="text/javascript">
// 1. 設置父頁面的 hash 值
parent.location.hash = "world";
</script>
postMessage 跨域
window.postMessage(message,targetOrigin) 方法是 HTML5 新引進的特性,可以使用它來向其它的 window 對象發送消息,無論這個 window 對象是屬於同源或不同源。這個應該就是以後解決 dom 跨域通用方法了。
調用 postMessage 方法的 window 對象是指要接收消息的那一個 window 對象,該方法的第一個參數 message 為要發送的消息,類型只能為字元串;第二個參數 targetOrigin 用來限定接收消息的那個 window 對象所在的域,如果不想限定域,可以使用通配符 *。
需要接收消息的 window 對象,可是通過監聽自身的 message 事件來獲取傳過來的消息,消息內容儲存在該事件對象的 data 屬性中。
頁面 http://www.laixiangran.cn/a.html
的代碼:
<iframe src="http://laixiangran.cn/b.html" id="myIframe" onload="test()" style="display: none;">
<script>
// 1. iframe載入 "http://laixiangran.cn/b.html 頁面後會執行該函數
function test() {
// 2. 獲取 http://laixiangran.cn/b.html 頁面的 window 對象,
// 然後通過 postMessage 向 http://laixiangran.cn/b.html 頁面發送消息
var iframe = document.getElementById('myIframe');
var win = iframe.contentWindow;
win.postMessage('我是來自 http://www.laixiangran.cn/a.html 頁面的消息', '*');
}
</script>
頁面 http://laixiangran.cn/b.html
的代碼:
<script type="text/javascript">
// 註冊 message 事件用來接收消息
window.onmessage = function(e) {
e = e || event; // 獲取事件對象
console.log(e.data); // 通過 data 屬性得到發送來的消息
}
</script>