3

什么是跨域

我们先看下以下场景:
开启两个本地服务器,页面A为localhost:9800,其中嵌套了iframeB localhost:9000,页面A想使用页面B的数据,例如调用它的方法,会报以下错误

clipboard.png

如图所示,Protocols,domains,and ports must match. 译为:协议、主机和端口号必须符合,否则,就是跨域。

下面我们来具体谈谈。

浏览器的同源策略

我们都知道,浏览器有个同源策略,也就是这个策略,限制了两个源中的资源相互交互。

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、LocalStorage 和 IndexDB 无法读取。
  • DOM 无法获得。
  • AJAX 请求不能发送。

而跨域访问,也无非两种情况:一是请求不同源下的接口(如上第3种);二是请求不同源的页面的资源(如上1、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 = 'test.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

结语

关于跨域的就先聊到这啦~


ripple07
838 声望77 粉丝

爱前端,爱喵星人( =•ω•= )