什么是瀏覽器同源策略
同源策略(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>
