美文网首页
有关跨域的相关问题和方法

有关跨域的相关问题和方法

作者: JRG_Orange | 来源:发表于2017-06-24 16:54 被阅读0次

    跨域是什么

    同源策略

    在讲解什么是跨域之前先要清楚什么是同源策略,“同源政策”(same-origin policy)是浏览器安全的基石。下面将讲解同源策略的相关概念。

    1.1 含义

    1995年,同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策。

    最初,它的含义是指,A网页设置的 Cookie,B网页不能打开,除非这两个网页“同源”。所谓“同源”指的是”三个相同“,即:

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

    1.2 目的

    同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。

    设想这样一种情况:A网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取A网站的 Cookie,会发生什么?

    很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。

    由此可见,“同源政策”是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。

    1.3 限制范围

    随着互联网的发展,“同源政策”越来越严格。目前,如果非同源(也称非本域),共有三种行为受到限制:

    (1) Cookie、LocalStorage 和 IndexedDB 无法读取。
    (2) DOM 无法获得。
    (3) AJAX 请求不能发送。

    虽然这些限制是必要的,但是有时很不方便,合理的用途也受到影响。下面,我将详细介绍,如何规避上面三种限制。

    如何实现跨域

    1. JSONP实现跨域

    JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。

    它的基本思想是,网页通过添加一个<script>元素,向服务器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。
    首先,网页动态插入<script>元素,由它向跨源网址发出请求。

     $('.change').addEventListener('click', function(){
        var script = document.createElement('script');
        script.src = 'http://localhost:8080/getNews?callback=appendHtml';
        document.head.appendChild(script);
        document.head.removeChild(script);
      })
    

    上面代码通过动态添加<script>元素,向服务器localhost:8080发出请求。注意,该请求的查询字符串有一个callback参数,用来指定回调函数的名字,这对于JSONP是必需的。
    让我们看看后台是怎么处理请求的:

    var cb = req.query.callback;
        if(cb){
            res.send(cb + '('+ JSON.stringify(data) + ')');
        }else{
            res.send(data);
        }
    

    服务器收到这个请求以后,如果发现请求的query中有先前约定好的callback参数,就会将后台数据放在回调函数callback的参数位置返回,否则直接返回后台数据。注意返回的是字符串。
    在前端的HTML里我们已经定义了callback()函数如下:(callabck=appendHtml)

    function appendHtml(news){
        var html = '';
        for( var i=0; i<news.length; i++){
          html += '<li>' + news[i] + '</li>';
        }
        console.log(html);
        $('.news').innerHTML = html;
      }
    

    如此一来,后台的数据就会展现在前端页面,实现了跨域访问数据。

    2. CORS实现跨域

    CORS是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是W3C标准,是跨源AJAX请求的根本解决方法。相比JSONP只能发GET请求,CORS允许任何类型的请求。

    2.1 简介

    CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
    整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
    后台的实现:

        res.header("Access-Control-Allow-Origin", "*"); 
        res.send(data);
    

    上面的代码在响应头添加了Access-Control-Allow-Origin:*,让所有网站可以访问该网站后台的数据。

    2.2 两种请求

    浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

    只要同时满足以下两大条件,就属于简单请求。

    (1)请求方法是以下三种方法之一:

    HEAD
    GET
    POST

    (2)HTTP的头信息不超出以下几种字段:

    Accept
    Accept-Language
    Content-Language
    Last-Event-ID
    Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

    凡是不同时满足上面两个条件,就属于非简单请求。

    浏览器对这两种请求的处理,是不一样的。

    2.3 简单请求的处理

    对于简单请求,浏览器直接发出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字段的值。
    相对JSONP而言,CORS与其使用目的相同,但是比JSONP更强大。
    JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

    tip:对于非简单请求(比如:请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。可以在阮一峰JS标准参考教程中查看

    3. iframe

    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方法打开的窗口,只要跨域,父窗口与子窗口之间就无法通信。

    3.1 降域

    如果两个窗口一级域名相同,只是二级域名不同,那么设置document.domain属性,就可以规避同源策略,拿到DOM,把这种方法也叫做降域。
    举例来说,A网页是//a.jirengu.com/a.html,B网页是//b.jirengu.com/b.html,那么只要设置相同的document.domain,两个网页就可以相互访问数据。
    在两个网站对应的HTML在都要设置:

    document.domain = "jrg.com"
    

    tip:document.domain还可以使两个一级域名相同,只是二级域名不同的网站共享 Cookie。

    3.2 对于完全不同源的网站,目前有两种方法,可以解决跨域窗口的通信问题:

    片段识别符(fragment identifier)
    跨文档通信API(Cross-document messaging)

    a)片段标识符(fragment identifier)指的是,URL的#号后面的部分,比如http://example.com/x.html#fragment的#fragment。如果只是改变片段标识符,页面不会重新刷新。

    父窗口可以把信息,写入子窗口的片段标识符:

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

    子窗口通过监听hashchange事件得到通知:

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

    同样的,子窗口也可以改变父窗口的片段标识符:

    parent.location.href= target + “#” + hash;
    

    b)window.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);
      }
    }
    

    小结

    本文介绍了同源策略,跨域的概念,简单介绍了JSONP、CROS以及iframe使用场景下通过降域、片段标识符、以及postMessage API来实现跨域的方法,让我们对跨域的实现有了较为全面的认识。本文的例子来自饥人谷官方视频教程阮一峰JS标准参考教程(alpha)

    相关文章

      网友评论

          本文标题:有关跨域的相关问题和方法

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