跨域方式知多少

作者: huangyh_max | 来源:发表于2017-06-21 17:25 被阅读172次

    前言

    好记性不如烂笔头,所以学完跨域之后,我还是老实结合demo来整理一篇跨域实现方式的详记。当然,因为我现在学习阶段,并没有跨域的实战经历,所以这篇整理,纯粹停留在我对跨域的理解层面。
      等后面接触到更多跨域知识,以及经历项目实战的跨域处理,有更透彻的进阶理解,就再做跨域知识的补充或跨域实战的记录。
      这次说的跨域方式有四种:

    1、JSONP
    2、 CORS
    3、 降域(document.domain)
    4、 postMessage
    

    一、为何要跨域——因为“同源策略”

    1、什么是跨域
    通俗来说,就是两个不同域名的网站的JavaScript脚本的交互。
    常见交互有:一方发送请求要获取数据,对方响应并传输数据;操作网站页面的DOM元素等。
    

    2、为何要跨域

    现实场景中,肯定有很多时候是需要跨域请求、传输数据的。这些合理的用途会被浏览器默认阻止。

    那么重点来了,为什么浏览器会默认阻止跨域操作呢?
      因为所有浏览器都奉行“同源策略”

    同源策略:只允许同源的JS脚本(或者说接口)进行交互。不同源的情况下,不能读写对方的任何资源。
    

    举例来说,http://www.jianshu.com这个网址,http://协议,www.jianshu.com是域名,端口号不写的情况下默认是80
      那么,同源需要满足下面三点,任何一点不同就视为不同源:

    1、同协议:常见协议有http://、https://、file://(本地文件)、ftp://协议
    2、同域名:比如www.example.com/dir2/other.html和www.example.com/dir/page.html
    3、同端口号:URL默认不写端口,默认端口就是80。
    (注意默认80端口和8080端口是不同的,两者不等同。)
    

    同源策略是浏览器出于信息安全考虑,防止恶意网站窃取用户数据。
      例如用户在登录某一网上银行网站后,又去登录其他恶意网站。如果没有同源策略,恶意网站的后台就可以获取网银网页的用户信息。因为浏览器对提交表单并没有同源策略的限制,假如用户登录网银后忘记退出登录。因为登录密码信息存储在cookie中,恶意网站就可以获取用户登录密码等信息,冒充用户,进行恶意操作。

    二、使用Ajax跨域请求失败例子

    在讲解跨域实现的方式之前,先来实例演示下,不使用任何跨域方式,只是纯粹ajax技术跨域或同域名请求数据时浏览器会做出的默认举动:
      例子:当前页面显示3个新闻标题,点击“换一组”按钮,从指定接口获取数据,随机生成新的3个新闻标题。

    index.html文件的代码:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>ajax获取数据</title>
        <style>
            .container{
                width: 900px;
                margin: 0 auto;
            }
        </style>
    </head>
    <body>
        <div class="container">
            <ul class="news">
                <li>随机变换新闻标题1</li>
                <li>随机变换新闻标题2</li>
                <li>随机变换新闻标题3</li>
            </ul>
            <button class="change">换一组</button>
        </div>
        <script>
            function $(str){
                return document.querySelector(str);
            }
            var change=$('.change');
            /*ajax的写法*/
            change.addEventListener('click',function(){
                var xhr=new XMLHttpRequest();
                xhr.open('get','http://bbhuangyh.com:8080/getNews',true); //指定接口获取数据
                xhr.send();
                xhr.onreadystatechange=function(){
                    if(xhr.readyState===4 && (xhr.status===200 || xhr.status===304)){
                        //将服务端返回的数据,作为自定义appendHtml()函数的参数
                        appendHtml(JSON.parse(xhr.responseText)) 
                    }
                }
            })
    
            function appendHtml(news){
                var html='';
                for(var i=0; i<news.length;i++){
                    html+='<li>'+news[i]+'</li>';
                }
                //console.log(html);
                $('.news').innerHTML=html;
            }
    
        </script>
    </body>
    </html>
    

    在模拟的服务端,返回JSON格式的数据:

    app.get('/getNews',function(req,res){
        var news=[
            "输入内容1",
            "输入内容2",
            "输入内容3",
            "输入内容4",
            "输入内容5",
            "输入内容6",
            "输入内容7",
            "输入内容8",
            "输入内容9"
        ]
        var data=[];
        for(var i=0;i<3;i++){
            var index=parseInt(Math.random()*news.length); //随机选择news数组的下标
            data.push(news[index]);
        }
        /*ajax的写法*/
        res.send(data);
        //console.log(data) //随机得到:[ '输入内容1', '输入内容4', '输入内容3' ]
    })
    
    1、同源域名登录的情况

    设置host文件,让www.bbhuangyh.com和www.aahuangyh.com等于127.0.0.1。然后使用http://bbhuangyh.com:8080/域名登录。此时网页的域名和数据请求接口的域名xhr.open('get','http://bbhuangyh.com:8080/getNews',true)相同。点击“换一组”按钮,就能从接口返回数据,获取替换页面数据,实现效果。

    334.JPG
    2、不同源域名登录的情况

    数据请求接口的域名是'http://bbhuangyh.com:8080/getNews,而现在使用localhost:8080或者http://aahuangyh.com:8080/这个完全不同的域名(即使实际都是指向127.0.0.1)来登录,点击“换一组”。这种情况下就是,不同源请求获取数据,浏览器会进行阻拦。点击“换一组”的操作无效。

    数据请求情况 preview没有数据返回 response没有数据返回

    浏览器返回的错误提醒:

    XMLHttpRequest cannot load http://bbhuangyh.com:8080/getNews. 
    No 'Access-Control-Allow-Origin' header is present on the requested resource. 
    Origin 'http://localhost:8080' is therefore not allowed access.
    

    三、跨域方法——JSONP

    1、JSONP的原理

    (1) <script> 标签是不受同源策略的限制的,它可以载入任意地方的 JavaScript 文件。
      (2)对页面script元素的src指定接口路径。默认执行src所指路径,就会向后台发起跨域数据请求。
      (3)在前端自定义会执行某项操作的函数。前端后台共同约定好这个自定义函数名。
      (4)后端响应数据请求后,对响应数据进行包装:把响应数据转化为JSON格式的字符串,然后用约定好的函数名称包裹住。然后将这包装后的 JSON数据返回到前端的script标签中,作为JS语句在script中执行。这样恰好就可以调用执行约定好的函数,并且将数据作为参数传入。

    下面demo例子中的src路径执行后的返回包装数据

    通过这样的实现思路,其实就是把跨域的冲突给消解了,变成是执行获取script元素的src所指内容,而通过后台数据包装,src所指内容获取到的返回数据,刚好能够执行前端html定义的指定函数。所以在这种情况下,只要后端服务有打开,前端的html文件即使通过file协议在浏览器打开,也能正常执行获取数据。

    适用情况:

    (1)前后端间会有接触,能至少共同约定通过URL传递的参数名称(如例子中的callback,通过req.query.callback得到函数名)或者是共同约定自定义的函数名称。
    (2)JSONP只能发GET请求

    2、JSONP的实例

    前面讲解了原理,我们现在来看下具体的例子吧:
      讲解这个跨域方法,我们还是使用前面点击“换一组”的例子demo。但JSONP的实现和Ajax没任何关系了,所以我们不再需要创建新的XMLHttpRequest对象。其本质是通过script标签的src属性来实现的,并且仍然需要服务器端支持,让src的link链接有数据返回。
      对应的demo例子index.html文件代码改写如下:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>JSONP获取数据</title>
        <style>
            .container{
                width: 900px;
                margin: 0 auto;
            }
        </style>
    </head>
    <body>
        <div class="container">
            <ul class="news">
                <li>随机变换新闻标题1</li>
                <li>随机变换新闻标题2</li>
                <li>随机变换新闻标题3</li>
            </ul>
            <button class="change">换一组</button>
        </div>
        <script>
            function $(str){
                return document.querySelector(str);
            }
            var change=$('.change');
            // /*JSONP的写法*/
            change.addEventListener('click',function(){
                var script=document.createElement('script');
                script.src='http://bbhuangyh.com:8080/getNews?callback=appendLi';
                //callback为传给后端的参数,值为appendLi
                //appendLi为约定好的函数名,可随意更改,只要和前端定义好的函数名一样就行
                document.head.appendChild(script);
                document.head.removeChild(script);
            })
            function appendLi(news){
                var html='';
                for(var i=0; i<news.length;i++){
                    html+='<li>'+news[i]+'</li>';
                }
                console.log(html);
                $('.news').innerHTML=html;
            }
    
        </script>
    </body>
    </html>
    

    在模拟的服务端,改写的代码:

    app.get('/getNews',function(req,res){
        var news=[
            "输入内容1",
            "输入内容2",
            "输入内容3",
            "输入内容4",
            "输入内容5",
            "输入内容6",
            "输入内容7",
            "输入内容8",
            "输入内容9"
        ]
        var data=[];
        for(var i=0;i<3;i++){
            var index=parseInt(Math.random()*news.length); //随机选择news数组的下标
            data.push(news[index]);
        }
        var cb=req.query.callback; 
        //这里的callback是自定义的名字,跟随html的url请求里的自定义名称一样就行。
        //优化做法:可以满足普通get方法获取,也可以jsonp方法获取
        if(cb){
            res.send(cb+'(' +JSON.stringify(data) + ')');
              //JSON.stringify(data)转化为JSON格式的字符串
             //console.log(cb+'(' +JSON.stringify(data) + ')')随机得到:appendLi(["输入内容8","输入内容6","输入内容4"])
        }else{
            res.send(data);
        }
    })
    

    使用localhost:8080、aahuangyh.com或者file协议打开都能随机返回三条数据做显示,实现跨域

    localhost:8080打开
    aahuangyh.com打开

    四、跨域方法——CORS

    前面举了个使用AJAX跨域失败的例子。而现在如果使用CORS的跨域方法的话,前面AJAX写法的demo,就能跨域生效。
      CORS是跨源资源分享(Cross-Origin Resource Sharing)的缩写。“它是W3C标准,是跨源AJAX请求的根本解决方法。”——引用

    1、实现方法

    CORS跨域方法的重点就在于,在后台代码上写上:res.header(“Accsess-Control-Allow-Origin”,“*”)
      一般,使用ajax跨域请求时,浏览器会在请求头Request headers加上“origin:发起请求的域名”
      在后台代码加上res.header(“Accsess-Control-Allow-Origin”,“*”),这样会让浏览器在后端响应头Response headers加上“Accsess-Control-Allow-Origin:*”*号表示对任何域名发送的请求,都给予回应和答应。

    2、兼容性

    header('Access-Control-Allow-Origin:*')是html5新增的一项标准功能。我们可以来看看CORS的兼容适用情况:

    CORS的兼容情况.JPG
      可以看到,IE11以下是不支持的。,无法兼容低版本的IE浏览器来使用。但胜在CORS用法简单,支持所有类型的HTTP请求。使用普通的XMLHttpRequest对象,用AJAX的写法就能实现跨域。
    3、实例

    来看实例demo吧:
      还是前面AJAX跨域demo,html部分的写法相同,模拟后台部分的代码添加上这一句:

        app.get('/getNews',function(req,res){
        var news=[
            "AAA",
            "BBB",
            "CCC",
            "DDD",
            "EEE",
            "FFF",
            "GGG",
            "HHH",
            "III"
        ];
        var data=[];
        for(var i=0;i<3;i++){
            var index=parseInt(Math.random()*news.length);
            data.push(news[index]);
        }
        //cors跨域写法
        res.header("Access-Control-Allow-Origin","*");
        res.send(data);
        })
    
    CORS方式下的Response Headers

    当然,也可以采用指定写法:res.header(“Accsess-Control-Allow-Origin”,“具体URL域名”),这样浏览器就只会对这个指定的URL域名的请求进行回应。
    例如:

    res.header("Access-Control-Allow-Origin","http://bbhuangyh.com:8080");
    

    就是指定允许http://bbhuangyh.com:8080来跨域访问xhr.open('get','http://aahuangyh.com:8080/getNews',true);指定的这个接口资源

    使用指定的对应域名打开正常获取数据 使用别的域名来打开就会报错
    (4)其他

    当然,还有其他一些配置。例如允许浏览器携带cookie来访问接口资源,后台要写:Access-Control-Allow-Credentials: true
    对应的“Accsess-Control-Allow-Origin:*”就不能是*号。而且前端部分的代码就需要配合写上:

    var xhr=new XMLHttpRequest();
    xhr.open('get',URL,true); //指定接口获取数据
    xhr.withCredentials = true; //允许浏览器携带cookie来访问接口,前端配合要写这句
    xhr.send();
    xhr.onreadystatechange=handler;
    

    五、跨域方法——降域

    (1)适用情况

    降域方法的实现是使用document.domain适用情景其实非常有限小众,常见于控制<iframe>窗口。比如当前页面下,有个iframe,其src地址是和我当前页面的网址域名不同域,但二级域名相同的情况下,而我需要对iframe的元素进行操作,这种场景下,我就使用降域方法。

    (2)降域说明

    什么是降域,实际举个例子,一目了然:
      比如我当前页面的网址URL是:http://a.huangyh.com:8080/a.html <iframe>的src地址是:http://b.huangyh.com:8080/b.html
      只有在两个域名是协议相同、端口相同、二级域名相同的情况下,使用document.domain="huangyh.com"降域,都只取域名的huangyh.com这部分,形同同域,然后在a.html和b.html都可以使用window对象对彼此进行dom操作。

    (3)demo实例

    在这个demo中,实现的是页面上有两个input,其中一个是iframe标签下打开的另一个页面的input。然后在其中任意一个input输入内容,另一个input会显示相同的内容。
      (1)当前页面a.html的代码:点击查看
      (2)iframe引用的b.html的代码:点击查看。需在服务端运行的情况下,查看效果。

    通过对比来加深理解和印象:
      对着两部分代码,可以先看看不降域,先设置同源的情况是如何:
      (1)在当前页面的URL和iframe的src路径相同的情况下:

    <body>
        <div class="ct">
            <h1>使用降域实现跨域</h1>
            <div class="main">
                <input type="text" placeholder="http://a.huangyh.com:8080/a.html">
            </div>
            <iframe src="http://a.huangyh.com:8080/b.html" frameborder="0"></iframe>
        </div>
    </body>
    </html>
    

    (2)window.frames[0].document.body.querySelector('#input'),就能够操作到右侧iframe窗口的input。因为本质是同源的。

    降域截图.jpg

    (3)在左边Input输入值,右边input同步

    现在,来修改iframe的src,与页面的URL不同域,就会报错:

    <div class="main">
    <input type="text" placeholder="http://a.huangyh.com:8080/a.html">
    </div>
    <iframe src="http://a.huangyh.com:8080/b.html" frameborder="0"></iframe>
    
    不降域的情况下报错

    这种情况下,就进行降域:
      在两边的html的script中都加入“document.domain="huangyh.com"”。结果两边的input输入都可以同步,实现页面URL和iframe的src不相同的情况下,也能跨域处理:

    降域下的跨域操作 降域下的跨域操作结果.JPG

    六、跨域方法——postMessage

    (1)实现方式

    HTML5中新增了window对象的window.postMessage方法,可以实现不同源的域名之间发送、监听消息。具体:

    发送消息:windowObj.postMessage(message, targetOrigin);

    • windowObj:需要接收消息的window对象
    • message:发送的消息
    • targetOrigin:接收消息的window对象所在的域名。可以是*号,也可以指定一个明确的URL

    监听消息:使用监听事件message

    • oragin:发送消息过来的域名
    • data:发送的数据
    • source:发送消息的window对象
    (2)demo实例

    简单说明之后,还是来看例子吧。
      这里还是使用降域的这个例子,实现一样的操作效果。但换成postMessage的方式来做,对原有代码做一些改动:
      a.html中的script部分:

    <script>
    document.querySelector('.main input').addEventListener('input',function(){
    window.frames[0].postMessage(this.value,'*');
    //window.frames[0]指iframe窗口
    //postmessage是向外发送消息,把input输入的内容发送到是任何域名的iframe窗口上,因为有*号
    })
    window.addEventListener('message',function(e){
    document.querySelector('.main input').value=e.data;
    //自己所在的页面,监听消息,如果监听到消息,就把数据放到input上
    })
    </script>
    

    b.html中的script部分:

    <script>
    document.querySelector('#input').addEventListener('input',function(){
    window.parent.postMessage(this.value,'*')
    //window.parent指iframe窗口的父窗口
    //postmessage是向外发送消息,把input输入的内容发送到是任何域名的父窗口上
    })
    window.addEventListener('message',function(e){
    document.querySelector('#input').value=e.data;
    //自己所在的页面,监听消息,如果监听到消息,就把数据放到input上
    })
    </script>
    

    实际在模拟后台运行后,打开a.html查看效果,结果在页面url和iframe的地址完全不同,也可以实现跨域数据互通:

    postMessage跨域效果 跨域实现两个input的数据互递

    后又后记:

    这篇文断续写了挺久。写的时候,对跨域的认识相比学的时候更清晰了些。常翻常新。

    参考文章:

    https://segmentfault.com/a/1190000006908944
    https://segmentfault.com/a/1190000003642057
    http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html

    相关文章

      网友评论

      本文标题:跨域方式知多少

      本文链接:https://www.haomeiwen.com/subject/osenfxtx.html