跨域

作者: 扶不起的蝌蚪 | 来源:发表于2020-04-27 23:57 被阅读0次

    同源策略

    url的组成部分
    只有当协议域名端口三者一致才算同源。
    在默认情况下 http 可以省略端口 80, https 省略 443,所以:
    http://www.example.com:80===http://www.example.com
    https://www.example.com:443===https://www.example.com

    没有同源策略限制的危险性

    没有同源策略限制的接口查询

    有一个小小的东西叫cookie大家应该知道,一般用来处理登录等场景,目的是让服务端知道谁发出的这次请求。如果你请求了接口进行登录,服务端验证通过后会在响应头加入Set-Cookie字段,然后下次再发请求的时候,浏览器会自动将cookie附加在HTTP请求的头字段Cookie中,服务端就能知道这个用户已经登录过了。知道这个之后,我们来看场景:
    1.你准备去清空你的购物车,于是打开了买买买网站www.maimaimai.com,然后登录成功,一看,购物车东西这么少,不行,还得买多点。
    2.你在看有什么东西买的过程中,你的好基友发给你一个链接www.nidongde.com
    3.你饶有兴致地浏览着www.nidongde.com,谁知这个网站暗地里做了些不可描述的事情!由于没有同源策略的限制,它向www.maimaimai.com发起了请求!聪明的你一定想到上面的话“服务端验证通过后会在响应头加入Set-Cookie字段,然后下次再发请求的时候,浏览器会自动将cookie附加在HTTP请求的头字段Cookie中”,这样一来,这个不法网站就相当于登录了你的账号,可以为所欲为了!
    这就是传说中的CSRF攻击
    看了这波CSRF攻击我在想,即使有了同源策略限制,但cookie是明文的,还不是一样能拿下来。于是我看了一些cookie相关的文章聊一聊 cookieCookie/Session的机制与安全,知道了服务端可以设置httpOnly,使得前端无法操作cookie,如果没有这样的设置,像XSS攻击就可以去获取到cookieWeb安全测试之XSS;设置secure,则保证在https的加密通信中传输以防截获。

    没有同源策略限制的Dom查询

    1.有一天你刚睡醒,收到一封邮件,说是你的银行账号有风险,赶紧点进www.yinghang.com改密码。你吓尿了,赶紧点进去,还是熟悉的银行登录界面,你果断输入你的账号密码,登录进去看看钱有没有少了。
    2.睡眼朦胧的你没看清楚,平时访问的银行网站是www.yinhang.com,而现在访问的是www.yinghang.com,这个钓鱼网站做了什么呢?

    // HTML
    <iframe name="yinhang" src="www.yinhang.com"></iframe>
    // JS
    // 由于没有同源策略的限制,钓鱼网站可以直接拿到别的网站的Dom
    const iframe = window.frames['yinhang']
    const node = iframe.document.getElementById('你输入账号密码的Input')
    console.log(`拿到了这个${node},我还拿不到你刚刚输入的账号密码吗`)
    

    跨域的处理方法

    1.JSONP
    JSONP用于IE<=9, Opera<12, Firefox<3.5 或者更加老的浏览器

    JSONP主要就是利用了script标签没有跨域限制的这个特性来完成的,但是JSONP只能发GET请求,因为本质上script加载资源就是GET

    流程:

    1. 前端定义解析函数如jsonpCallback=function(){....}
    2. script标签的srcparams形式包装请求参数,并且声明执行函数(例如 cb=jsonpCallback)
    3. 后端获取前端声明的执行函数(jsonpCallback),并以带上参数并调用执行函数的方式传递给前端。
    //前端
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
      </head>
      <body>
        <script type='text/javascript'>
          // 后端返回直接执行的方法,相当于执行这个方法,由于后端把返回的数据放在方法的参数里,所以这里能拿到res。
          window.jsonpCallback= function (res) {
            console.log(res)
          }
        </script>
        <script src='http://localhost:9871/api/jsonp?msg=helloJsonp&cb=jsonpCallback' type='text/javascript'></script>
      </body>
    </html>
    
    后端
    const {successBody} = require('../utli')
    class CrossDomain {
      static async jsonp (ctx) {
        // 前端传过来的参数
        const query = ctx.request.query
        // 设置一个cookies
        ctx.cookies.set('tokenId', '1')
        // query.cb是前后端约定的方法名字,其实就是后端返回一个直接执行的方法给前端,由于前端是用script标签发起的请求,所以返回了这个方法后相当于立马执行,并且把要返回的数据放在方法的参数里。
        ctx.body = `${query.cb}(${JSON.stringify(successBody({msg: query.msg}, 'success'))})`
      }
    }
    module.exports = CrossDomain
    
    JQuery Ajax 示例
    <script>
      $.ajax({
        url: "http://localhost:8080/api/jsonp",
        dataType: "jsonp",
        type: "get",
        data: {
          msg: "hello"
        },
        jsonp: "cb", //指定返回执行的函数
        success: function(data) {
          console.log(data);
        }
      });
    </script>
    

    JSONP请求封装

    /**
     * JSONP请求工具
     * @param url 请求的地址
     * @param data 请求的参数
     * @returns {Promise<any>}
     */
    const request = ({url, data}) => {
      return new Promise((resolve, reject) => {
        // 处理传参成xx=yy&aa=bb的形式
        const handleData = (data) => {
          const keys = Object.keys(data)
          const keysLen = keys.length
          return keys.reduce((pre, cur, index) => {
            const value = data[cur]
            const flag = index !== keysLen - 1 ? '&' : ''
            return `${pre}${cur}=${value}${flag}`
          }, '')
        }
        // 动态创建script标签
        const script = document.createElement('script')
        // 接口返回的数据获取
        window.jsonpCb = (res) => {
          document.body.removeChild(script)
          delete window.jsonpCb
          resolve(res)
        }
        script.src = `${url}?${handleData(data)}&cb=jsonpCb`
        document.body.appendChild(script)
      })
    }
    // 使用方式
    request({
      url: 'http://localhost:9871/api/jsonp',
      data: {
        // 传参
        msg: 'helloJsonp'
      }
    }).then(res => {
      console.log(res)
    })
    

    2. iframe + form

    const requestPost = ({url, data}) => {
      // 首先创建一个用来发送数据的iframe.
      const iframe = document.createElement('iframe')
      iframe.name = 'iframePost'
      iframe.style.display = 'none'
      //创建一个空的iframedocument.body.appendChild(iframe)
      const form = document.createElement('form')
      const node = document.createElement('input')
      // 注册iframe的load事件处理程序,如果你需要在响应返回时执行一些操作的话.
      iframe.addEventListener('load', function () {
        console.log('post success')
      })
    
      form.action = url
      // 在指定的iframe中执行form
      form.target = iframe.name
      form.method = 'post'
      for (let name in data) {
        node.name = name
        node.value = data[name].toString()
        form.appendChild(node.cloneNode())
      }
      // 表单元素需要添加到主文档中.
      form.style.display = 'none'
      
      document.body.appendChild(form)
      form.submit()
    
      // 表单提交后,就可以删除这个表单,不影响下次的数据发送.
      document.body.removeChild(form)
    }
    // 使用方式
    requestPost({
      url: 'http://localhost:9871/api/iframePost',
      data: {
        msg: 'helloIframePost'
      }
    })
    

    3.CORS

    CORS需要浏览器和服务器同时支持。IE>9, Opera>=12, or Firefox>=3.5的浏览器支持

    整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

    因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

    CORS将请求分为简单请求复杂请求

    简单请求要同时满足以下条件

    1. 请求方法为如下之一
    • GET
    • POST
    • HEAD
    1. HTTP的头信息(header)不超出以下几种字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
    1. Content-Type的值仅限于下列三者之一:(例如 application/json 为非简单请求)
    • text/plain
    • tmultipart/form-data
    • tapplication/x-www-form-urlencoded

    对于简单请求,浏览器直接发出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字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequestonerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

    如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

    Access-Control-Allow-Headers: Content-Type, Accept, Authorization,DomainName
    Access-Control-Allow-Methods: GET,HEAD,POST,PUT,DELETE,TRACE,OPTIONS,PATCH
    Access-Control-Allow-Origin: *
    Access-Control-Expose-Headers: Content-Type, Accept, Authorization,DomainName
    Allow: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH
    Connection: keep-alive
    Date: Mon, 27 Apr 2020 15:10:15 GMT
    Server: nginx/1.17.6
    Transfer-Encoding: chunked
    

    上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。

    • Access-Control-Allow-Origin
      该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

    • Access-Control-Allow-Credentials
      该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

    • Access-Control-Expose-Headers
      该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。

    CORScookie问题
    想要传递cookie需要满足 3 个条件

    1. withCredentials
    // 原生 xml 的设置方式
    var xhr = new XMLHttpRequest();
    xhr.withCredentials = true;
    // axios 设置方式
    axios.defaults.withCredentials = true;
    //但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie
    //这时,可以显式关闭withCredentials
    //xhr.withCredentials = false;
    

    2.Access-Control-Allow-Credentialstrue

    Access-Control-Allow-Credentials: true
    
    1. Access-Control-Allow-Origin不能是*

    CORSCookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

    复杂请求

    非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

    浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

    //下面是一段浏览器的JavaScript脚本。
    var url = 'http://api.alice.com/cors';
    var xhr = new XMLHttpRequest();
    xhr.open('PUT', url, true);
    xhr.setRequestHeader('X-Custom-Header', 'value');
    xhr.send();
    

    上面代码中,HTTP请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header

    浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的HTTP头信息。

    若服务器对预检请求没有任何响应,那么浏览器不知道服务器是否支持CORS而不会发送后续的实际请求;或者服务器不支持当前的Origin跨域访问也不会发送后续请求。

    Accept: */*
    Accept-Encoding: gzip, deflate
    Accept-Language: zh-CN,zh;q=0.9
    Access-Control-Request-Headers: content-type
    Access-Control-Request-Method: POST
    Connection: keep-alive
    DNT: 1
    Host: targetDomain
    Origin: youDomain
    User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3722.400 QQBrowser/10.5.3751.400
    

    "预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,除了关键字段是Origin,表示请求来自哪个源。还有以下关键字段

    • Access-Control-Request-Method
      该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是POST
    • Access-Control-Request-Headers
      该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是content-type

    注意事项:
    在新版的chrome 中,如果你发送了复杂请求,你却看不到options请求。可以在这里设置chrome://flags/#out-of-blink-cors设置成disbale,重启浏览器。对于非简单请求就能看到options请求了。

    **4.Node 正向代理 **

    代理的思路为,利用服务端请求不会跨域的特性,让接口和当前站点同域。

    代理前 代理后
    • Webpack (4.x)
    const path = require("path");
    const HtmlWebpackPlugin = require("html-webpack-plugin");
    
    module.exports = {
      entry: {
        index: "./index.js"
      },
      output: {
        filename: "bundle.js",
        path: path.resolve(__dirname, "dist")
      },
      devServer: {
        port: 8000,
        proxy: {
          "/api": {
            target: "http://localhost:8080"
          }
        }
      },
      plugins: [
        new HtmlWebpackPlugin({
          filename: "index.html",
          template: "webpack.html"
        })
      ]
    };
    
    • Vue-cli 2.x
    // config/index.js
    
    ...
    proxyTable: {
      '/api': {
         target: 'http://localhost:8080',
      }
    },
    ...
    
    • Vue-cli 3.x
    module.exports = {
      devServer: {
        port: 8000,
        proxy: {
          "/api": {
            target: "http://localhost:8080"
          }
        }
      }
    };
    

    4. Nginx 反向代理(后端)

    5.Websocket

    WebSocket规范定义了一种 API,可在网络浏览器和服务器之间建立“套接字”连接。简单地说:客户端和服务器之间存在持久的连接,而且双方都可以随时开始发送数据。详细教程可以看https://www.html5rocks.com/zh/tutorials/websockets/basics/

    这种方式本质没有使用了 HTTP 的响应头, 因此也没有跨域的限制,没有什么过多的解释直接上代码吧。

    <script>
      let socket = new WebSocket("ws://localhost:8080");
      socket.onopen = function() {
        socket.send("秋风的笔记");
      };
      socket.onmessage = function(e) {
        console.log(e.data);
      };
    </script>
    

    5. document.domain + Iframe

    级域名原理

    .com 一级域名
    baidu. com 二级域名
    www. baidu. com 三级域名

    该方式只能用于二级域名相同的情况下,比如a.test.comb.test.com适用于该方式。 只需要给页面添加document.domain ='test.com'表示二级域名都相同就可以实现跨域。

    // a.test.com
    <body>
      helloa
      <iframe
        src="http://b.test.com/b.html"
        frameborder="0"
        onload="load()"
        id="frame"
      ></iframe>
      <script>
        document.domain = "test.com";
        function load() {
          console.log(frame.contentWindow.a);
        }
      </script>
    </body>
    
    // b.test.com
    <body>
      hellob
      <script>
        document.domain = "test.com";
        var a = 100;
      </script>
    </body>
    

    6. location.hash + iframe

    实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframelocation.hash传值,相同域之间直接js访问来通信。

    具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。

    //a.html:(http://www.domain1.com/a.html))
    <script>
        var iframe = document.getElementById('iframe');
    
        // 向b.html传hash值
        setTimeout(function() {
            iframe.src = iframe.src + '#user=admin';
        }, 1000);
        
        // 开放给同域c.html的回调方法
        function onCallback(res) {
            alert('data from c.html ---> ' + res);
        }
    </script>
    
    b.html:(http://www.domain2.com/b.html))
    <iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
    <script>
        var iframe = document.getElementById('iframe');
    
        // 监听a.html传来的hash值,再传给c.html
        window.onhashchange = function () {
            iframe.src = iframe.src + location.hash;
        };
    </script>
    
    c.html:(http://www.domain1.com/c.html))
    <script>
        // 监听b.html传来的hash值
        window.onhashchange = function () {
            // 再通过操作同域a.html的js回调,将结果传回
            window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
        };
    </script>
    

    7.window.name+ Iframe

    window 对象的 name 属性是一个很特别的属性,当该 window 的 location 变化,然后重新加载,它的 name 属性可以依然保持不变。

    其中 a.html 和 b.html 是同域的,都是http://localhost:8000,而 c.html 是http://localhost:8080

    // a.html
    <iframe
      src="http://localhost:8080/name/c.html"
      frameborder="0"
      onload="load()"
      id="iframe"
    ></iframe>
    <script>
      let first = true;
      // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
      function load() {
        if (first) {
          // 第1次onload(跨域页)成功后,切换到同域代理页面
          iframe.src = "http://localhost:8000/name/b.html";
          first = false;
        } else {
          // 第2次onload(同域b.html页)成功后,读取同域window.name中数据
          console.log(iframe.contentWindow.name);
        }
      }
    </script>
    
    //b.html 为中间代理页,与 a.html 同域,内容为空。
    // b.html
    <div></div>
    
    // c.html
    <script>
      window.name = "秋风的笔记";
    </script>
    

    通过 iframe 的 src 属性由外域转向本地域,跨域数据即由 iframe 的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

    8.window.postMessage

    postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
    a.) 页面和其打开的新窗口的数据传递
    b.) 多窗口之间消息传递
    c.) 页面与嵌套的iframe消息传递
    d.) 上面三个场景的跨域数据传递

    用法:postMessage(data,origin)方法接受两个参数
    data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
    origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。

    <iframe
      src="http://localhost:8080"
      frameborder="0"
      id="iframe"
      onload="load()"
    ></iframe>
    <script>
      function load() {
        iframe.contentWindow.postMessage("秋风的笔记", "http://localhost:8080");
     //onmessage 是window属性
        window.onmessage = e => {
          console.log(e.data);
        };
      }
    </script>
    
    <div>hello</div>
    <script>
      window.onmessage = e => {
        console.log(e.data); // 秋风的笔记
        e.source.postMessage(e.data, e.origin);
      };
    </script>
    

    相关文章

      网友评论

          本文标题:跨域

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