跨域那些事兒

什麼是跨域

咱們先看下如下場景:
開啓兩個本地服務器,頁面A爲localhost:9800,其中嵌套了iframeB localhost:9000,頁面A想使用頁面B的數據,例如調用它的方法,會報如下錯誤javascript

clipboard.png

如圖所示,Protocols,domains,and ports must match. 譯爲:協議、主機和端口號必須符合,不然,就是跨域。html

下面咱們來具體談談。java

瀏覽器的同源策略

咱們都知道,瀏覽器有個同源策略,也就是這個策略,限制了兩個源中的資源相互交互。chrome

Two pages have the same origin if the protocol, port (if one is specified), and host are the same for both pages.

譯爲:若是兩個頁面有相同的協議、端口號和主機,那麼這兩個頁面就屬於同一個源。跨域

也就是說,只有同一個源才能夠進行資源共享。瀏覽器

咱們來舉幾個例子,若是想和 http://www.test.com/index.html 進行通訊:安全

URL Result Reason
http://www.test.com/page/othe... 容許
http://www.test.com/index.js 容許
http://a.test.com/index.html 不容許 子域不一樣
http://www.other.com/index.html 不容許 主域不一樣
https://www.test.com/index.html 不容許 協議不一樣
http://www.test.com:3000/index.html 不容許 端口不一樣

咱們能夠看到,協議、端口、主機缺一不可,必須徹底匹配,上文就是因爲端口號不一樣而報錯。服務器

那爲何要有同源策略的限制呢?緣由也很簡單,就是爲了保證用戶信息的安全,防止惡意的網站竊取數據。cookie

試想一下,若是咱們能夠隨意訪問一個網站的cookie,那麼豈不是隨意竊取別別人登錄的cookie?若是沒有同源策略,那互聯網就太危險了。app

跨域的幾種常見方案

同源策略的限制範圍有如下幾種:

  • Cookie、LocalStorage 和 IndexDB 沒法讀取。
  • DOM 沒法得到。
  • AJAX 請求不能發送。

而跨域訪問,也無非兩種狀況:一是請求不一樣源下的接口(如上第3種);二是請求不一樣源的頁面的資源(如上一、2,一般是頁面中嵌套不一樣源的iframe要進行通訊)。本文主要討論第二種狀況下形成的跨域,方法不少,主要介紹如下幾種:document.domainlocation.hashwindow.namepostMessage

document.domain

適用於:主域相同子域不一樣的頁面,例如上例中的第三種
方法:只需將這兩個頁面的document.domain設置爲相同的父域名,便可實現不一樣子域名之間的跨域通訊。

<!-- http://www.test.com/index.html -->  
<button>發送消息</button>  
<div></div>  
<iframe name="ifr" src="http://a.test.com/index.html" style="width: 100%;"></iframe>  

<script type="text/javascript">  

    document.domain = 'test.com';  
    function sendMsg(msg) {  
        document.querySelector('div').innerHTML = msg;
    }
    
    document.querySelector('button').onclick = function (){  
        window.frames['ifr'].sendMsg('Hello son');
    }
    
</script>
<!-- http://a.test.com/index.html -->  
<div></div> 

<script type="text/javascript">  

    document.domain = 'myapp.com';  
    function sendMsg(msg) {  
        document.querySelector('div').innerHTML = msg;
        parent.sendMsg('Hello father');
    }
    
</script>

clipboard.png

location.hash

通常來講,URL的任何改變都從新會加載一個新的網頁,除了hash的變化,hash的任何改變都不會致使頁面刷新。

在跨域解決方案中,hash也很是有用,來自不一樣源的頁面能夠相互設置對方的 URL,包括hash值,但不能獲取對方的hash值。文檔之間能夠經過hash來相互通訊。

clipboard.png

流程(如上圖):

  1. 主頁面A中嵌入iframeB,兩個來自不一樣域
  2. 主頁面A中,將想要傳遞給B的字段,做爲hash,將它與B的url鏈接起來,而後將B的src設置爲鏈接後的url
  3. iframeB中,就能夠經過獲取本身url的hash值,從而獲得主頁面傳遞的值,但在iframeB中,需設置一個定時器監聽hash值的變化

除了設置定時器,還能夠經過監聽window.onhashchange事件

例子:啓動兩個本地服務器,主頁面localhost:9800,子頁面localhost:9000,主頁面向子頁面發消息

<!-- 主頁面 localhost:9800 -->
<script type="text/javascript">

     function sendMsg(msg) {
         var data = encodeURI(JSON.stringify({msg: msg}));
         var target = "http://localhost:9000";
         var src = target + "#" + data;
         document.getElementById("ifr").src = src;
    }
    function onClick() {
         sendMsg("來自'localhost: 9800': 你好, 這是你要的數據");
    }
    
</script>

<body>
<iframe id="ifr" src="http://localhost:9000" style="width: 100%"></iframe>
<button onclick="onClick()">發送消息</button>
</body>
<!-- 子頁面 localhost:9000 -->
<script type="text/javascript">

    var oldHash = "";
    function checkHash() {
        var newHash = location.hash.length > 1 ? location.hash.substring(1) : '';
        if (newHash != oldHash) {
            oldHash = newHash;
            var msg = JSON.parse(decodeURI(newHash)).msg;
            document.getElementById("container").innerText = msg;
            clearInterval(timer);
        }
    }
    //設置定時器監聽 hash 的變化
    var timer = setInterval(checkHash, 1000);
    
</script>

<body>
<div id="container"></div>
</body>

結果以下:點擊button後,子頁面將收到主頁面的消息

clipboard.png

注意:使用 hash時最好對其進行編碼、解碼,不然在 Firefox中會報錯。由於 Firefox會自動將 hash值進行編碼,若是不進行解碼就沒法 JSON.parse().

兩個頁面不一樣源的狀況下,IE、Chrome不容許修改parent.location.hash的值(Firefox能夠),因此若是主頁面想從子頁面獲取消息,只能藉助一個代理iframe設置。

clipboard.png

流程以下(兩種):

  1. 子頁面建立隱藏的代理iframe,與主頁面同源,並將消息做爲hash,設置到iframe的src中
  2. 代理頁面將主頁面的hash值設置爲自身的hash
  3. 主頁面使用定時器監聽hash的變化

例子以下

<!-- 主頁面 localhost:9800 -->
<script type="text/javascript">

    var oldHash = '';
    function checkHash() {
        var newHash = location.hash.length > 1 ? location.hash.substring(1) : '';
        if (newHash != oldHash) {
            oldHash = newHash;
            var msg = JSON.parse(decodeURI(newHash)).msg;
            document.querySelector("div").innerText = msg;
            clearInterval(timer);
        }
    }
    
    //設置定時器監聽 hash 的變化
    var timer = setInterval(checkHash, 1000);

</script>

<body>
<iframe id="ifr" name="ifr" src="http://localhost:9000" style="width: 100%;"></iframe>
<div></div>
</body>
<!-- 子頁面 localhost:9000 -->
<script type="text/javascript">

    function sendMsg(msg) {
        try {
            parent.location.hash = encodeURI(JSON.stringify({msg: msg}));
        } catch(e) {
            //因爲IE, Chrome的安全機制 沒法修改 parent.location.hash
            //所以建立代理頁面, 與主頁面同源, 而後修改代理頁面的hash
            var ifrProxy = document.createElement("frame");
            ifrProxy.style.display = "none";
            ifrProxy.src = "http://localhost:9800/public/proxy.html" + '#' + encodeURI(JSON.stringify({msg: msg}));
            document.body.appendChild(ifrProxy);
        }
    }
    function onClick() {
        sendMsg("Hello Father");
    }
    
</script>

<body>
<button onclick="onClick()">發送消息</button>
</body>
<!-- 代理頁面 localhost:9800/public/proxy.html -->
<script type="text/javascript">

    parent.parent.location.hash = window.location.hash.substring(1);
    
</script>

結果如圖:

clipboard.png

這種方法的劣處就是將消息暴露在url中,所以也能夠採用下文將講述利用代理iframe跨域的方法,這邊就不贅述了。

因爲如今許多網站的hash已經被用於其餘用途,此時想用hash跨域就會比較複雜了。這種狀況下,咱們可使用一個同域的代理頁面來完成

頁面A想向頁面B發送數據,流程以下:

  1. 頁面A建立一個隱藏的代理iframeC,這個iframe與頁面B同域
  2. 頁面A中,將要發送的數據,做爲hash,與頁面Curl鏈接起來
  3. 代理iframeC中,它與B同域,能夠直接調用頁面B中的方法,這樣即可以將hash值傳遞給B了

例子以下

<!-- 頁面A localhost:9800 -->
<body>
<iframe name="ifr" src="http://localhost:9000" style="width: 100%;"></iframe>
<button onclick="onClick()">發送消息</button>
<div></div>

<script type="text/javascript">

    function sendMsg(msg) {
        //建立隱藏的iframe, 與頁面B同源
        var frame = document.createElement('frame');
        var target = 'http://localhost:9000/static/page/proxy.html';
        var data = {frameName: 'ifr', msg: msg};
        frame.src = target + "#" + encodeURI(JSON.stringify(data));
        frame.style.display = 'none';
        document.body.appendChild(frame);
    }
    function onClick() {
        sendMsg('你好, 我是localhost: 9800');
    }

</script>
<!-- 代理頁面C localhost:9000/static/page/proxy.html -->
<script type="text/javascript">

    var hash = location.hash.length > 1 ? location.hash.substring(1) : '';
    if (hash) {
        var data = JSON.parse(decodeURI(hash));
        //處理數據
        parent.frames[data.frameName].receiveData(data.msg);
    }

</script>
<!-- 頁面B localhost:9000 -->
<script type="text/javascript">

    function receiveData(msg) {
        document.querySelector('div').innerHTML = "接收到數據:"+ msg;
    }
        
</script>

<body>
<div></div>
</body>

結果以下:

clipboard.png

缺點:

  1. 數據直接暴露在url中,安全性較低
  2. url大小是有限制的,它支持傳遞的數據量較小

window.name

加載任何頁面 window.name 的值始終保持不變

當頁面A想從頁面B中獲取數據時:
clipboard.png

  1. 頁面A,建立一個隱藏的iframeC,將C的src指向頁面B
  2. 頁面C加載完成後,把響應的數據附加到window.name
  3. C 取到數據後,將src設爲任何一個與A同源的頁面,這時 A 就能獲取到 B 的name屬性值
  4. A 取到數據後,隨時能夠刪掉 C

例子,啓動兩個本地服務器,頁面Alocalhost:9800,頁面Blocalhost:9000,頁面A想從頁面B中獲取數據

<!-- 頁面A localhost:9800 -->
<script type="text/javascript">

    function sendMsg(msg) {
        var state = 0, data;
        //1. 建立隱藏的iframe, 與頁面B同源
        var frame = document.createElement("frame");
        frame.src = "http://localhost:9000";
        frame.style.display = "none";
        frame.onload = function () {
            if (state === 1) {
                //3. 此時iframe與頁面A同源, 頁面A能夠獲取到數據
                data = frame.contentWindow.name;
                document.querySelector('.res').innerHTML = "響應數據:" + data;
                //4. 刪除iframe
                frame.contentWindow.document.write('');
                frame.contentWindow.close();
                document.body.removeChild(frame);
            } else {
                //2. iframe加載完成後, 響應的數據已附加到此iframe上, 再將其導航至與頁面A同源
                state = 1;
                frame.src = "http://localhost:9800/test.html";
            }
        };
        document.body.appendChild(frame);
    }
    function onClick() {
        var val = 'hi, 我是頁面A';
        sendMsg(val);
        document.querySelector('.val').innerHTML = "請求數據:" + val;
    }
    
</script>

<body>
<button onclick="onClick()">發送消息</button>
<div class="val"></div>
<div class="res"></div>
</body>
<!-- 頁面B localhost:9000 -->
<script type="text/javascript">
    window.name = 'hi, 我是頁面B';
</script>

結果如圖

clipboard.png

另外,兩個頁面還可相互通訊
頁面A經過hash,將數據傳遞給頁面B,頁面B仍經過window.name向頁面A傳遞數據

場景以下:頁面B存儲了一些人的信息,頁面B經過頁面A的輸入,獲取不一樣人的信息
clipboard.png

<!-- 頁面A localhost:9800 -->
<script type="text/javascript">

    function sendMsg(msg) {
        var state = 0, data;
        //1. 建立隱藏的iframe, 與頁面B同源
        var frame = document.createElement("frame");
        frame.style.display = "none";
        //將獲取的數據經過hash傳遞給頁面B
        frame.src = "http://localhost:9000" + "#" + encodeURI(JSON.stringify({data: msg}));
        frame.onload = function () {
            if (state === 1) {
                //3. 此時iframe與頁面A同源, 頁面A能夠獲取到數據
                data = JSON.parse(frame.contentWindow.name);
                var html = '';
                for (var key in data) {
                    html += key + ": " + data[key] + " ";
                }
                document.querySelector('.res').innerHTML = html;
                //4. 刪除iframe
                frame.contentWindow.document.write('');
                frame.contentWindow.close();
                document.body.removeChild(frame);
            } else {
                //2. iframe加載完成後, 響應的數據已附加到此iframe上, 再將其導航至與頁面A同源,
                state = 1;
                // !!!注意 '/test.html'不可缺乏,不然在firefox不會觸發frame.onload, 在chrome雖會觸發但會報錯
                frame.src = "http://localhost:9800/test.html";
            }
        };
        document.body.appendChild(frame);
    }
    function onClick() {
        var val = document.querySelector('.text').value;
        sendMsg(val);
        document.querySelector('.val').innerHTML = "id爲 " + val + " 的信息以下:";
    }
    
</script>

<body>
<input class="text" type="text" />
<button onclick="onClick()">發送消息</button>
<div class="val"></div>
<div class="res"></div>
</body>
<!-- 頁面B localhost:9000 -->
<script type="text/javascript">

    var information = {
        '1': {
            name: '狐狸',
            age: 2
        },
        '2': {
            name: '饅頭',
            age: 1
        },
        '3': {
            name: '端午',
            age: 3
        }
    };
    var hash = location.hash.length > 1 ? location.hash.substring(1) : '';
    if (hash) {
        //獲取hash值 並進行轉換
        var obj = JSON.parse(decodeURI(hash));
        var data = obj.data;
        //傳遞的數據爲object, 需進行JSON轉換
        window.name = JSON.stringify(information[data]);
    }
    
</script>

輸入想得到的數據的id值,便可獲得相應的信息

clipboard.png

總結:
優勢:容量很大,能夠放置很長的字符串(2M左右,比url大得多)
缺點:必需要監聽window.name屬性的變化

window.postMessage()

HTML5規範中的新方法window.postMessage()能夠用於安全跨域通訊。當該方法調用時,將分發一個消息事件。對應的窗口經過事件監聽來獲取消息。

語法 :otherWindow.postMessage(message, targetOrigin)

otherWindow表明其餘窗口的引用,例如iframe的contentWindow屬性,經過window.open返回的窗體,經過window.frames[]返回的ifame對象。

message表示發送給其餘窗口的數據

targetOrigin指定哪些來源的窗口能夠接收到消息事件,其值能夠是字符串"*"(表示無限制)或"/"(表示與父窗口同源)或一個URI。發送消息時,只有目標窗口的協議主機端口這三項都匹配targetOrigin提供的值,消息纔會發送。

接收消息的窗口能夠經過監聽message事件,去接收消息

window.addEventListener("message", receiveMessage, false);

receiveMessage爲接收到消息後的操做

message事件的事件對象event,提供三個屬性:

  • event.source:發送消息的窗體(能夠用來引用父窗口)
  • event.origin:消息發向的網址(能夠過濾不是發給本窗口的消息)
  • event.data:消息內容

例子:啓動兩個本地服務器,頁面Alocalhost:9800,頁面Blocalhost:9000,頁面A根據頁面B發來的數據改變顏色

<!-- 頁面A localhost:9800 -->
<body>
<iframe name="ifr" src="http://localhost:9000" style="width: 100%; display: none"></iframe>
<div style="width: 200px; height: 200px; background-color: #ccc"></div>
<button onclick="onClick()">改變顏色</button>

<script type="text/javascript">

    function onClick() {
        var otherFrame = window.frames["ifr"];
        otherFrame.postMessage("getColor", "*");
    }
    function handleReceive(event){
        //判斷來源
        if(event.origin != "http://localhost:9000")
            return;
        //處理頁面B發送的數據
        var data = JSON.parse(event.data);
        document.querySelector('div').style.backgroundColor = data.color;
    }
    window.addEventListener("message", handleReceive, false);
    
</script>
<!-- 頁面B localhost:9000 -->
<script type="text/javascript">

    function handleReceive(event){
        //判斷來源
        if (event.origin != "http://localhost:9800") {
            return;
        }
        if(event.data == "getColor"){
            //給頁面A發送數據
            var targetWindow = parent.window;
            targetWindow.postMessage(JSON.stringify({color:'blue'}), "http://localhost:9800");
        }
    }
    window.addEventListener("message", handleReceive, false);

</script>

結果以下:點擊按鈕後,頁面的div由灰變藍

clipboard.png

clipboard.png

結語

關於跨域的就先聊到這啦~

相關文章
相關標籤/搜索