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


什么是瀏覽器同源策略

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

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

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

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

另外,同源策略又分為以下兩種:

  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>

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM