BOM 瀏覽器對象模型_同源限制

「同源政策」(same-origin policy)javascript

瀏覽器安全的基石
html

  • 協議相同
  • 域名相同
  • 端口相同

1995年,同源政策由 Netscape 公司引入瀏覽器。目前,全部瀏覽器都實行這個政策前端

  • A 網頁設置的 Cookie,B 網頁不能打開,除非這兩個網頁「同源」
  • 瀏覽器同時還規定,提交表單不受同源政策的限制

目前,若是非同源,共有如下三項受到限制java

  • 沒法讀取非同源網頁的 Cookie、LocalStorage 和 IndexedDB。
  • 沒法接觸非同源網頁的 DOM
  • 沒法向非同源地址發送 AJAX 請求(能夠發送,但瀏覽器會拒絕接受響應)

經過 JavaScript 腳本能夠拿到其餘窗口的 window 對象。若是是非同源的網頁,目前容許一個窗口能夠接觸其餘網頁的window對象的九個屬性和四個方法web

window.closed
window.frames
window.length
window.location        惟一一個能夠用讀寫的屬性
window.opener
window.parent
window.self
window.top
window.window
window.blur()
window.close()
window.focus()
window.postMessage()json

 

目的: 保證用戶信息的安全,防止惡意的網站竊取數據
api

Cookie 是服務器寫入瀏覽器的一小段信息,只有同源的網頁才能共享。跨域

只適用於 Cookie 和 iframe 窗口瀏覽器

若是兩個網頁一級域名相同,只是次級域名不一樣,瀏覽器容許經過設置 document.domain 共享 Cookie
緩存

A 網頁的網址是 http://w1.example.com/a.html

B 網頁的網址是http://w2.example.com/b.html

那麼只要設置相同的 document.domain ,兩個網頁就能夠共享 Cookie。由於瀏覽器經過 document.domain 屬性來檢查是否同源

  • // 兩個網頁都須要設置
    document.domain = 'example.com';

注意,A 和 B 兩個網頁都須要設置 document.domain 屬性,才能達到同源的目的。

由於設置 document.domain 的同時,會把端口重置爲 null

所以若是隻設置一個網頁的 document.domain,會致使兩個網址的端口不一樣,仍是達不到同源的目的

如今,A 網頁經過腳本設置一個 Cookie ---- document.cookie = "test1=hello";

B 網頁就能夠讀到這個 Cookie ---- var allCookie = document.cookie;

另外,服務器也能夠在設置 Cookie 的時候,指定 Cookie 的所屬域名爲一級域名,好比.example.com

  • Set-Cookie: key=value; domain=.example.com; path=/

    這樣的話,二級域名 和 三級域名 不用作任何設置,均可以讀取這個 Cookie

對於徹底不一樣源的網站,目前有兩種方法,能夠解決跨域窗口的通訊問題

1. 片斷識別符(fragment identifier)

片斷標識符(fragment identifier)指的是,URL 的 # 號後面的部分

好比 http://example.com/x.html#fragment 的 #fragment若是隻是改變片斷標識符,頁面不會從新刷新

  • 父窗口能夠把信息,寫入子窗口的片斷標識符 ---- 子窗口經過監聽 hashchange 事件獲得通知

父窗口

  • var src = originURL + '#' + data;
    document.getElementById('myIFrame').src = src;

子窗口 iframe

  • window.onhashchange = checkMessage;
    
    function checkMessage() {
        var message = window.location.hash;
        // ...
    }

一樣的,子窗口也能夠改變父窗口的片斷標識符

  • parent.location.href = target + '#' + hash;

上面的這種方法屬於破解,HTML5 爲了解決這個問題,引入了一個全新的API:跨文檔通訊 API(Cross-document messaging)

2. 跨文檔通訊API(Cross-document messaging)

這個 API 爲window對象新增了一個window.postMessage方法,容許跨窗口通訊,不論這兩個窗口是否同源

舉例來講,父窗口 aaa.com 向子窗口 bbb.com 發消息,調用 postMessage 方法就能夠了

  • // 父窗口打開一個子窗口
    var popup = window.open('http://bbb.com', 'title');
    // 父窗口向子窗口發消息 popup.postMessage('Hello World!', 'http://bbb.com');

子窗口向父窗口發送消息的寫法相似

  • // 子窗口向父窗口發消息
    window.opener.postMessage('Nice to see you', 'http://aaa.com');

父窗口和子窗口均可以經過 message 事件,監聽對方的消息

  • // 父窗口和子窗口均可以用下面的代碼,
    // 監聽 message 消息
    window.addEventListener('message', function (e) {
      console.log(e.data);
    },false);

message事件的參數是事件對象event,提供如下三個屬性。

  • event.source        發送消息的窗口
  • event.origin        消息發向的網址
  • event.data        消息內容

子窗口經過 event.source 屬性引用父窗口,而後發送消息

  • window.addEventListener('message', receiveMessage);
    function receiveMessage(event) {
        event.source.postMessage('Nice to see you!', '*');
    }

注意: 

  • receiveMessage 函數 裏面沒有過濾信息的來源,任意網址發來的信息都會被處理
  • postMessage 方法 中指定的目標窗口的網址是一個星號,表示該信息能夠向任意網址發送

一般來講,這兩種作法是不推薦的,由於不夠安全,可能會被惡意利用

event.origin 屬性能夠過濾不是發給本窗口的消息

  • window.addEventListener('message', receiveMessage);
    function receiveMessage(event) {
        if (event.origin !== 'http://aaa.com') return;
        if (event.data === 'Hello World') {
            event.source.postMessage('Hello', event.origin);
        } else {
            console.log(event.data);
        } 
    }

第一個參數 ---- 具體的信息內容

第二個參數 ---- 接收消息的窗口的源(origin),即「協議 + 域名 + 端口」。也能夠設爲 * ,表示不限制域名,向全部窗口發送

經過 window.postMessage,讀寫其餘窗口的 LocalStorage 也成爲了可能

  • 父窗口發送消息的代碼以下
  • var win = document.getElementsByTagName('iframe')[0].contentWindow;
    var obj = { name: 'Jack' };
    win.postMessage(
        JSON.stringify({key: 'storage', data: obj}),
        'http://bbb.com'
    );
  • 子窗口將父窗口發來的消息,寫入本身的 LocalStorage
  • window.onmessage = function(e) {
        if (e.origin !== 'http://bbb.com') {
            return;
        }
        var payload = JSON.parse(e.data);
        localStorage.setItem(payload.key, JSON.stringify(payload.data));
    };
  • 增強版的父窗口發送消息代碼以下。
  • var win = document.getElementsByTagName('iframe')[0].contentWindow;
    var obj = { name: 'Jack' };
    
    // 存入對象
    win.postMessage(JSON.stringify({key: 'storage', method: 'set', data: obj}), 'http://bbb.com');
    
    // 讀取對象
    win.postMessage(JSON.stringify({key: 'storage', method: "get"}), "*");
    
    window.onmessage = function(e) {
        if (e.origin != 'http://aaa.com'){
            return;
        }
        console.log(JSON.parse(e.data).name);
    };
  • 增強版的子窗口接收消息的代碼以下
  • window.onmessage = function(e) {
        if (e.origin !== 'http://bbb.com'){
            return;
        }
        
        var payload = JSON.parse(e.data);
        switch (payload.method) {
            case 'set':
                localStorage.setItem(payload.key, JSON.stringify(payload.data));
                break;
            case 'get':
                var parent = window.parent;
                var data = localStorage.getItem(payload.key);
                parent.postMessage(data, 'http://aaa.com');
                break;
            case 'remove':
                localStorage.removeItem(payload.key);
                break;
        }
    };

解決 AJAX 請求(服務器與客戶端 交互) 跨域的 三種方法

JSONP 只能發 GET 請求

  • 特色: 

簡單適用,老式瀏覽器所有支持,服務端改造很是小

  • 基本思想: 

網頁經過動態插入一個 <script> 元素,向服務器請求 JSON 數據 ---- 這種 通常請求 的作法不受同源政策限制

服務器收到請求後,將數據放在一個指定名字的回調函數裏傳回來 ---- foo({"ip": "192.168.3.31"})

因爲 <script> 元素請求的腳本,直接做爲代碼運行

只要瀏覽器定義了 foo 函數,foo 函數就會當即調用。做爲參數的 JSON 數據 被視爲 JavaScript 對象

使用:

前端頁面

  • function foo(data) {
        console.log('Your public IP address is: ' + data.ip);
    };
    
    function addScriptTag(src) {
        var script = document.createElement('script');
        script.setAttribute("type","text/javascript");
        script.src = src;
        document.body.appendChild(script);
    }
    
    window.onload = function () {
        addScriptTag('http://example.com/ip?callback=foo');
    }

後臺服務器

  • 服務器收到這個請求之後,會將數據放在回調函數的參數位置返回
  • foo({"ip": "192.168.3.31"})

WebSocket 通訊協議

WebSocket 是一種通訊協議,使用 ws://(非加密)和 wss://(加密)做爲協議前綴

該協議不實行同源政策,幾乎全部的瀏覽器都支持,因此只要服務器支持,就能夠經過它進行跨源通訊

  • 瀏覽器發送請求實例
  • GET /chat HTTP/1.1
    Host:                          server.example.com
    Upgrade:                       websocket
    Connection:                    Upgrade
    Sec-WebSocket-Key:             x3JJHMbDL1EzLkh9GBhXDw==
    Sec-WebSocket-Protocol:        chat, superchat
    Sec-WebSocket-Version:         13
    Origin:                        http://example.com

服務器能夠根據 origin 這個字段,判斷是否許可本次通訊。

若是該域名在白名單內,服務器就會作出以下回應

  • HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
    Sec-WebSocket-Protocol: chat

CORS 跨源資源分享(Cross-Origin Resource Sharing) 運行任何類型的請求

是一個W3C 標準,也是 跨源 AJAX 請求 的根本解決方法

整個 CORS 通訊過程,都是瀏覽器自動完成

CORS 須要瀏覽器和服務器同時支持。目前,全部瀏覽器都支持該功能

實現 CORS 通訊的關鍵是服務器。因此,只要服務器實現了 CORS 接口,就能夠跨域通訊

  • 做用: 容許瀏覽器向跨域的服務器,發出 XMLHttpRequest 請求,從而克服了 AJAX 只能同源使用的限制
  • CORS 通訊 與 普通的 AJAX 通訊

沒有差異,代碼徹底同樣

瀏覽器一旦發現 AJAX 請求跨域,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感知

CORS 請求分紅兩類

  • 簡單請求(simple request)---- 簡單的 HTTP 方法與簡單的 HTTP 頭信息的結合

同時知足如下兩大條件

請求方法是 GET、POST、HEAD 其中之一

HTTP 的頭信息不超出如下幾種字段

xAccept
Accept-Language
Content-Language
Last-Event-ID
Content-Type        只限於三個值 application/x-www-form-urlencoded、multipart/form-data、text/plain

這樣劃分的緣由是,表單在歷史上一直能夠跨域發出請求。

簡單請求就是表單請求,瀏覽器沿襲了傳統的處理方式,不把行爲複雜化

對於非簡單請求,瀏覽器會採用新的處理方式

  • 基本流程

瀏覽器直接發出 CORS 請求

具體來講,就是在頭信息之中,增長一個 Origin 字段

Origin 字段用來講明,本次請求來自哪一個域(協議 + 域名 + 端口)

服務器根據這個值,決定是否贊成此次請求

  • GET /cors HTTP/1.1 Origin: http://api.bob.com
    Host: api.alice.com
    Accept-Language: en-US
    Connection: keep-alive
    User-Agent: Mozilla/5.0...

若是 Origin 指定的源,不在許可範圍內服務器會返回一個正常的 HTTP 迴應

瀏覽器發現,這個迴應的頭信息沒有包含 Access-Control-Allow-Origin 字段(詳見下文)就知道出錯了

從而拋出一個錯誤,被 XMLHttpRequest 的 onerror 回調函數捕獲

注意,這種錯誤沒法經過狀態碼識別,由於 HTTP 迴應的狀態碼有多是200

若是Origin指定的域名在許可範圍內,服務器返回的響應,會多出幾個頭信息字段

  • Access-Control-Allow-Origin

該字段是必須的

值要麼是請求時 Origin 字段的值;要麼是一個*,表示接受任意域名的請求

  • Access-Control-Allow-Credentials

該字段可選

值是一個布爾值,表示是否容許發送 Cookie

默認狀況下 false,Cookie 不包括在 CORS 請求之中

默認不包含 Cookie 信息(以及 HTTP 認證信息等),這是爲了下降 CSRF 攻擊的風險

設爲 true,即表示服務器明確許可,瀏覽器能夠把 Cookie 包含在請求中,一塊兒發給服務器

某些場合,服務器可能須要拿到 Cookie

1. 這時須要服務器顯式指定 Access-Control-Allow-Credentials字段,告訴瀏覽器能夠發送 Cookie

  • Access-Control-Allow-Credentials: true

2. 同時,開發者必須在 AJAX 請求中打開 withCredentials 屬性

不然,即便服務器要求發送 Cookie,瀏覽器也不會發送

或者說,服務器要求設置 Cookie,瀏覽器也不會處理

  • var xhr = new XMLHttpRequest();
    xhr.withCredentials = true;

注意:

 若是服務器要求瀏覽器發送 Cookie,Access-Control-Allow-Origin 就不能設爲星號,必須指定明確的、與請求網頁一致的域名

同時,Cookie 依然遵循同源政策,只有用服務器域名設置的 Cookie 纔會上傳

其餘域名的 Cookie 並不會上傳,且(跨域)原網頁代碼中的 document.cookie 也沒法讀取服務器域名下的 Cookie

這個值也只能設爲 true,若是服務器不要瀏覽器發送 Cookie,不發送該字段便可

  • Access-Control-Expose-Headers

該字段可選

CORS 請求時,XMLHttpRequest 對象的 getResponseHeader() 方法只能拿到6個服務器返回的基本字段

Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。

若是想拿到其餘字段,就必須在 Access-Control-Expose-Headers 裏面指定

下面的例子指定,getResponseHeader('FooBar')能夠返回FooBar字段的值

  • Access-Control-Allow-Origin: http://api.bob.com
    Access-Control-Allow-Credentials: true
    Access-Control-Expose-Headers: FooBar
    Content-Type: text/html; charset=utf-8
  • 非簡單請求(not-so-simple request)---- 

是那種對服務器提出特殊要求的請求

好比 請求方法是 PUT 或 DELETE,或者 Content-Type 字段的類型是 application/json

會在正式通訊以前,增長一次 HTTP 查詢請求,稱爲「預檢」請求 (preflight)

瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可使用哪些 HTTP 動詞和頭信息字段

只有獲得確定答覆,瀏覽器纔會發出正式的 XMLHttpRequest 請求,不然就報錯

這是爲了防止這些新增的請求,對傳統的沒有 CORS 支持的服務器造成壓力,給服務器一個提早拒絕的機會

這樣能夠防止服務器收到大量 DELETE 和 PUT 請求,這些傳統的表單不可能跨域發出的請求

舉個例子:

  • var url = 'http://api.alice.com/cors';
    var xhr = new XMLHttpRequest();
    xhr.open('PUT', url, true);
    xhr.setRequestHeader('X-Custom-Header', 'value');
    xhr.send();

上面代碼中,HTTP 請求的方法是PUT,而且發送一個自定義頭信息X-Custom-Header

  • 瀏覽器發現,這是一個非簡單請求,就自動發出一個「預檢」請求,要求服務器確承認以這樣請求。
  • 下面是這個「預檢」請求的 HTTP 頭信息
  • 「預檢」請求用的請求方法是OPTIONS,表示這個請求是用來詢問的
  • 頭信息裏面,關鍵字段是Origin,表示請求來自哪一個源
  • Access-Control-Request-Method ---- 該字段是必須的,用來列出瀏覽器的 CORS 請求會用到哪些 HTTP 方法,下例是PUT
  • Access-Control-Request-Headers ---- 該字段是一個逗號分隔的字符串,指定瀏覽器 CORS 請求會額外發送的頭信息字段,下例是X-Custom-Header
  • OPTIONS /cors HTTP/1.1
    Origin: http://api.bob.com
    Access-Control-Request-Method: PUT
    Access-Control-Request-Headers: X-Custom-Header
    Host: api.alice.com
    Accept-Language: en-US
    Connection: keep-alive
    User-Agent: Mozilla/5.0...

預檢請求的迴應

服務器收到 「預檢」 請求之後

檢查了 Origin、Access-Control-Request-Method Access-Control-Request-Headers 字段之後

  • 確認容許跨源請求,就能夠作出迴應
  • HTTP/1.1 200 OK
    Date: Mon, 01 Dec 2008 01:15:39 GMT
    Server: Apache/2.0.61 (Unix)
    Access-Control-Allow-Origin: http://api.bob.com
    Access-Control-Allow-Methods: GET, POST, PUT
    Access-Control-Allow-Headers: X-Custom-Header
    Content-Type: text/html; charset=utf-8
    Content-Encoding: gzip
    Content-Length: 0
    Keep-Alive: timeout=2, max=100
    Connection: Keep-Alive
    Content-Type: text/plain

上面的 HTTP 迴應中,關鍵的是 Access-Control-Allow-Origin 字段,表示http://api.bob.com能夠請求數據

該字段也能夠設爲星號,表示贊成任意跨源請求

若是服務器否認了「預檢」請求

會返回一個正常的 HTTP 迴應

可是沒有任何 CORS 相關的頭信息字段,或者明確表示請求不符合條件

  • OPTIONS http://api.bob.com HTTP/1.1
    Status: 200
    Access-Control-Allow-Origin: https://notyourdomain.com
    Access-Control-Allow-Method: POST

    這時,瀏覽器就會認定,服務器不一樣意預檢請求,所以觸發一個錯誤,被 XMLHttpRequest 對象的 onerror 回調函數捕獲

  • 控制檯會打印出以下的報錯信息

  • Access-Control-Allow-Methods: GET, POST, PUT
    Access-Control-Allow-Headers: X-Custom-Header
    Access-Control-Allow-Credentials: true
    Access-Control-Max-Age: 1728000
    
    XMLHttpRequest cannot load http://api.alice.com.
    Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
  • Access-Control-Allow-Methods

該字段必需,它的值是逗號分隔的一個字符串,代表服務器支持的全部跨域請求的方法。

注意,返回的是全部支持的方法,而不單是瀏覽器請求的那個方法。這是爲了不屢次「預檢」請求

  • Access-Control-Allow-Headers 

若是瀏覽器請求包括 Access-Control-Request-Headers 字段,則 Access-Control-Allow-Headers 字段是必需的

它也是一個逗號分隔的字符串,代表服務器支持的全部頭信息字段,不限於瀏覽器在「預檢」中請求的字段

  • Access-Control-Allow-Credentials

含義與同簡單請求相同

  • Access-Control-Max-Age

該字段可選,用來指定本次預檢請求的有效期,單位爲秒

上面結果中,有效期是20天(1728000秒)即容許緩存該條迴應1728000秒(即20天),在此期間,不用發出另外一條預檢請求

一旦服務器經過了「預檢」請求,之後每次瀏覽器正常的 CORS 請求,就都跟簡單請求同樣

會有一個 Origin 頭信息字段

服務器的迴應,也都會有一個 Access-Control-Allow-Origin 頭信息字段

JSONP 只支持GET請求,

CORS 支持全部類型的 HTTP 請求。

JSONP 的優點在於支持老式瀏覽器,以及能夠向不支持 CORS 的網站請求數據

相關文章
相關標籤/搜索