跨域

作者: ezrealor | 来源:发表于2017-08-30 15:42 被阅读0次

    什么是同源策略

    浏览器出于安全方面的考虑,只允许与本域下的接口交互。不同源的客户端脚本在没有明确授权的情况下,不能读写对方的资源。

    “同源”指的是”三个相同“。

    • 协议相同
    • 域名相同
    • 端口相同

    同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。
    随着互联网的发展,“同源政策”越来越严格。目前,如果非同源,共有三种行为受到限制。

    • Cookie、LocalStorage 和 IndexedDB 无法读取。
    • DOM 无法获得。
    • AJAX 请求无效(可以发送,但浏览器会拒绝接受响应)。
      虽然这些限制是必要的,但是有时很不方便,合理的用途也受到影响。下面,将详细介绍,如何规避上面三种限制。

    什么是跨域?跨域有几种实现形式

    跨域:允许不同域的接口进行交互
    实现方式:

    • JSONP
    • CORS
    • 降域
    • postMessage

    JSONP

    JSONP是一种数据调用方式,是解决JSON跨域获取的解决方案。因此JSONP其实是一个非官方的协议。
    JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。

    JSONP 的主要缺点有两个,
    是只能 GET 不能 POST,因为是通过<script>引用的资源,参数全都显式的放在URL里,和 AJAX 没有关系。
    是存在安全隐患,动态插入<script>标签其实就是一种脚本注入,可能遭遇XSS

    JSONP原理
    网页通过添加一个<script>元素,向服务器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。

    JSONP实现
    定义数据处理函数_fun
    创建script标签,src的地址执行后端接口,最后加个参数callback=_fun
    服务端在收到请求后,解析参数,计算返还数据,输出 fun(data) 字符串。
    fun(data)会放到script标签做为js执行。此时会调用fun函数,将data做为参数。

    <body> 
      <div class="container"> 
        <ul class="news"> 
          <li>第11日前瞻:中国冲击4金 博尔特再战</li> 
          <li>男双力争会师决赛 </li> 
          <li>女排将死磕巴西!</li> 
        </ul> 
        <button class="change">换一组</button> 
      </div>
    
    <script> 
      $('.change').addEventListener('click', function(){ 
        var script = document.createElement('script'); // 创建元素节点 
        script.src = 'http://localhost:8080/getNews?callback=appendHtml'; 
       // 引入后端接口  后面加上 callback=fun 确定函数名
        document.head.appendChild(script); 
       // 从后端返回的数据作为js代码执行 调用定义好的函数
        document.head.removeChild(script); 
      })
     function appendHtml(news){ 
        var html = ''; 
        for( var i=0; i<news.length; i++){ 
          html += '<li>' + news[i] + '</li>'; 
        } 
        $('.news').innerHTML = html; 
     } 
     function $(id){ 
       return document.querySelector(id); 
     } 
    </script> 
    </body> 
    

    app.get('/getNews', function(req, res){ 
      var news = [ 
         "第11日前瞻:中国冲击4金 博尔特再战200米羽球", 
         "正直播柴飚/洪炜出战 男双力争会师决赛", 
         "女排将死磕巴西!郎平安排男陪练模仿对方核心", 
         "没有中国选手和巨星的110米栏 我们还看吗?", 
         "中英上演奥运金牌大战",
       ] 
       var data = []; 
       for(var i=0; i<3; i++){ 
         var index = parseInt(Math.random()*news.length); 
         data.push(news[index]); 
         news.splice(index, 1); 
        } 
        var cb = req.query.callback; 
        if(cb){ 
          res.send(cb + '('+ JSON.stringify(data) + ')');  
           // JSON.stringify(arr) = "["aa","bb","cc"]" 
          //  '('+ JSON.stringify(data) + ')'  = '([data1,data2,data3....])'
        }else{ res.send(data); } 
    })
    

    CORS
    CORS是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是W3C标准,是跨源AJAX请求的根本解决方法。
    相比JSONP只能发GET请求,CORS允许任何类型的请求。
    CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
    整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
    因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

    CORS实现
    当你使用 XMLHttpRequest发送请求时,浏览器发现该请求不符合同源策略,会给该请求加一个请求头:Origin,后台进行一系列处理
    如果确定接受请求则在返回结果中加入一个响应头:Access-Control-Allow-Origin;
    浏览器判断该相应头中是否包含 Origin 的值
    如果有则浏览器会处理响应,我们就可以拿到响应数据,
    如果不包含浏览器直接驳回,这时我们无法拿到响应数据。
    所以 CORS 的表象是让你觉得它与同源的 ajax 请求没啥区别,代码完全一样。

    浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
    只要同时满足以下两大条件,就属于简单请求。
    请求方法是以下三种方法之一。
    HEAD
    GET
    POST
    HTTP的头信息不超出以下几种字段。
    Accept
    Accept-Language
    Content-Language
    Last-Event-ID
    Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
    凡是不同时满足上面两个条件,就属于非简单请求。
    浏览器对这两种请求的处理,是不一样的。

    基本流程
    对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。
    下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个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字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
    如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
    如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

    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
    

    上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。
    (1)Access-Control-Allow-Origin
    该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
    (2)Access-Control-Allow-Credentials
    该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
    (3)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字段的值。

    withCredentials 属性
    上面说到,CORS请求默认不包含Cookie信息(以及HTTP认证信息等)。如果需要包含Cookie信息,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。
    Access-Control-Allow-Credentials: true
    另一方面,开发者必须在AJAX请求中打开withCredentials属性。
    var xhr = new XMLHttpRequest();
    xhr.withCredentials = true;
    否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。
    但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials。
    xhr.withCredentials = false;
    需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

    简单例子:
    浏览器判断该相应头中是否包含 Origin 的值 
    如果不包含浏览器直接驳回,这时我们无法拿到响应数据。
    //前端
    $('.change').addEventListener('click', function () {
        var xhr = new XMLHttpRequest();
        xhr.open('get', 'http://b.jrg.com:8080/getNews', true);
        xhr.send();
        xhr.onreadystatechange = function () {
            if (xhr.readyState === 4 && xhr.status === 200) {
                appendHtml(JSON.parse(xhr.responseText))
            }
        };
        window.xhr = xhr
    });
    结果会发生报错:没有响应头被允许
    
    
     如果有则浏览器会处理响应,我们就可以拿到响应数据,
    //后端
    res.header("Access-Control-Allow-Origin", "http://a.jrg.com:8080");
    //res.header("Access-Control-Allow-Origin", "*");
    可实现CORS跨域:响应头被允许
    

    降域

    Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。但是,两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置document.domain共享 Cookie。(这种方法又被称为 降域)

    举例来说,A网页是http://w1.example.com/a.html,B网页是http://w2.example.com/b.html,那么只要设置相同的document.domain,两个网页就可以共享Cookie。
    注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexedDB 无法通过这种方法,规避同源政策,而要使用下文介绍的PostMessage API。
    另外,服务器也可以在设置 Cookie 的时候,指定 Cookie 的所属域名为一级域名,比如.example.com。
    Set-Cookie: key=value; domain=.example.com; path=/
    这样的话,二级域名和三级域名不用做任何设置,都可以读取这个Cookie。


    iframe元素可以在当前网页之中,嵌入其他网页。每个iframe元素形成自己的窗口,即有自己的window对象。iframe窗口之中的脚本,可以获得父窗口和子窗口。但是,只有在同源的情况下,父窗口和子窗口才能通信;如果跨域,就无法拿到对方的DOM。
    比如,父窗口运行下面的命令,如果iframe窗口不是同源,就会报错。

     document.getElementById("myIFrame").contentWindow.document
     // Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.
    

    上面命令中,父窗口想获取子窗口的DOM,因为跨域导致报错。
    反之亦然,子窗口获取主窗口的DOM也会报错。
    window.parent.document.body
    // 报错

    这种情况不仅适用于iframe窗口,还适用于window.open方法打开的窗口,只要跨域,父窗口与子窗口之间就无法通信。
    如果两个窗口一级域名相同,只是二级域名不同,那么设置上一节介绍的document.domain属性,就可以规避同源政策,拿到DOM。
    父页面 子页面的script代码里都加上
    doucument.domain = 'example.com'

    对于完全不同源的网站,目前有两种方法,可以解决跨域窗口的通信问题。
    片段识别符(fragment identifier)
    跨文档通信API(Cross-document messaging)


    postmessage

    HTML5为了解决跨域问题,引入了一个全新的API:跨文档通信 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');
    postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即“协议 + 域名 + 端口”。也可以设为*,表示不限制域名,向所有窗口发送。
    子窗口向父窗口发送消息的写法类似。
    window.opener.postMessage('Nice to see you', 'http://aaa.com');
    父窗口和子窗口都可以通过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);
    }
    }
    

    LocalStorage
    通过window.postMessage,读写其他窗口的 LocalStorage 也成为了可能。

     <div class="ct"> 
       <h1>使用postMessage实现跨域</h1> 
       <div class="main"> 
       <input type="text" placeholder="http://a.jrg.com:8080/a.html"> 
       </div> 
     <iframe src="http://localhost:8080/b.html" frameborder="0" ></iframe> 
     </div> 
    
     <script> 
     //URL: http://a.jrg.com:8080/a.html 
     $('.main input').addEventListener('input', function(){ 
       console.log(this.value); 
       window.frames[0].postMessage(this.value,'*'); // postMessage 
      }) 
     window.addEventListener('message',function(e) { 
       $('.main input').value = e.data 
       console.log(e.data); 
     }); 
    function $(id){ return document.querySelector(id); } 
    </script> 
    

     <input id="input" type="text" placeholder="http://b.jrg.com:8080/b.html"> 
    
    <script> 
    // URL: http://b.jrg.com:8080/b.html  
    $('#input').addEventListener('input', function(){ 
      window.parent.postMessage(this.value, '*'); // postMessage 
     }) 
    window.addEventListener('message',function(e) { 
       $('#input').value = e.data 
       console.log(e.data); 
     }); 
    function $(id){ return document.querySelector(id); } 
    </script>

    相关文章

      网友评论

          本文标题:跨域

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