瀏覽器同源策略及跨域的解決方法

什麼是瀏覽器同源策略

同源策略(Same origin policy)是一種約定,它是瀏覽器最核心也最基本的安全功能,若是缺乏了同源策略,則瀏覽器的正常功能可能都會受到影響。能夠說 Web 是構建在同源策略基礎之上的,瀏覽器只是針對同源策略的一種實現。javascript

它的核心就在於它認爲自任何站點裝載的信賴內容是不安全的。當被瀏覽器半信半疑的腳本運行在沙箱時,它們應該只被容許訪問來自同一站點的資源,而不是那些來自其它站點可能懷有惡意的資源。html

所謂同源是指:域名、協議、端口相同。java

下表是相對於 http://www.laixiangran.cn/home/index.html 的同源檢測結果:ajax

另外,同源策略又分爲如下兩種:json

  1. DOM 同源策略:禁止對不一樣源頁面 DOM 進行操做。這裏主要場景是 iframe 跨域的狀況,不一樣域名的 iframe 是限制互相訪問的。
  2. XMLHttpRequest 同源策略:禁止使用 XHR 對象向不一樣源的服務器地址發起 HTTP 請求。

爲何要有跨域限制

由於存在瀏覽器同源策略,因此纔會有跨域問題。那麼瀏覽器是出於何種緣由會有跨域的限制呢。其實不難想到,跨域限制主要的目的就是爲了用戶的上網安全。後端

若是瀏覽器沒有同源策略,會存在什麼樣的安全問題呢。下面從 DOM 同源策略和 XMLHttpRequest 同源策略來舉例說明:跨域

若是沒有 DOM 同源策略,也就是說不一樣域的 iframe 之間能夠相互訪問,那麼黑客能夠這樣進行攻擊:瀏覽器

  1. 作一個假網站,裏面用 iframe 嵌套一個銀行網站 http://mybank.com
  2. 把 iframe 寬高啥的調整到頁面所有,這樣用戶進來除了域名,別的部分和銀行的網站沒有任何差異。
  3. 這時若是用戶輸入帳號密碼,咱們的主網站能夠跨域訪問到 http://mybank.com 的 dom 節點,就能夠拿到用戶的帳戶密碼了。

若是沒有 XMLHttpRequest 同源策略,那麼黑客能夠進行 CSRF(跨站請求僞造) 攻擊:緩存

  1. 用戶登陸了本身的銀行頁面 http://mybank.comhttp://mybank.com 向用戶的 cookie 中添加用戶標識。
  2. 用戶瀏覽了惡意頁面 http://evil.com,執行了頁面中的惡意 AJAX 請求代碼。
  3. http://evil.comhttp://mybank.com 發起 AJAX HTTP 請求,請求會默認把 http://mybank.com 對應 cookie 也同時發送過去。
  4. 銀行頁面從發送的 cookie 中提取用戶標識,驗證用戶無誤,response 中返回請求數據。此時數據就泄露了。
  5. 並且因爲 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)。

只要同時知足如下兩大條件,就屬於簡單請求

  1. 請求方法是如下三種方法之一:
  • HEAD
  • GET
  • POST
  1. HTTP的頭信息不超出如下幾種字段:
  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限於三個值 application/x-www-form-urlencoded、multipart/form-data、text/plain

凡是不一樣時知足上面兩個條件,就屬於非簡單請求

瀏覽器對這兩種請求的處理,是不同的。

簡單請求

  1. 在請求中須要附加一個額外的 Origin 頭部,其中包含請求頁面的源信息(協議、域名和端口),以便服務器根據這個頭部信息來決定是否給予響應。例如:Origin: http://www.laixiangran.cn
  2. 若是服務器認爲這個請求能夠接受,就在 Access-Control-Allow-Origin 頭部中回發相同的源信息(若是是公共資源,能夠回發 * )。例如:Access-Control-Allow-Origin:http://www.laixiangran.cn
  3. 沒有這個頭部或者有這個頭部但源信息不匹配,瀏覽器就會駁回請求。正常狀況下,瀏覽器會處理請求。注意,請求和響應都不包含 cookie 信息。
  4. 若是須要包含 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.htmlhttp://laixiangran.cn/b.html 這兩個頁面的 document.domain 都設成相同的域名就能夠了。但要注意的是,document.domain 的設置是有限制的,咱們只能把 document.domain 設置成自身或更高一級的父域,且主域必須相同。例如:a.b.laixiangran.cn 中某個文檔的 document.domain 能夠設成 a.b.laixiangran.cnb.laixiangran.cnlaixiangran.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>
複製代碼

參考資料


轉載請註明出處,謝謝!

相關文章
相關標籤/搜索