JimQing's Blog


View JimQing's projecton GitHub

关于跨域问题详解

09 Aug 2019

前言

跨域,是Web开发中经常遇到而且在面试中也经常考查的问题,而跨域是什么,跨域的原因又是什么,我们要如何去解决跨域问题?

什么是跨域

跨域,其实是浏览器对JavaScript的一种限制,它限制不同域下的不同Js不能调用彼此。因为它判断此段请求,是不同域的,简而言之,一个域的网站去请求另一个域的资源,这段请求即是跨域的。而浏览器的判断规则,即同源策略。

什么是同源策略

同源策略即请求的地址域名,必须与当前页面的域名相同,包括协议和端口号。只要有其中一个不同,则浏览器会判断该请求跨域。同源策略是浏览器的行为,是为了保护本地数据不被JavaScript代码获取回来的数据污染,因此拦截的是客户端发出的请求回来的数据接收,即请求发送了,服务器响应了,但是无法被浏览器接收。

同源策略限制了:

Cookie、LocalStorage等存储型内容
Dom节点
Ajax请求(非同源)

注意: 标签则是允许跨域加载资源的。

如何解决跨域问题,跨域的方法?

在现实的开发中,我们经常需要进行跨域请求,那么跨域的方法又有什么呢?

以下列出三种跨域方式,可以适用于大部分的开发场景。

  1. JSONP

    JSONP是一种常见的且兼容性较好的前端跨域方式,在大部分浏览器上都可以实现跨域。唯一的缺点是它只支持Get请求,从而它的传输即是不安全的,容易遭受XSS攻击。

    JSONP主要使用了允许跨域的标签来进行跨域。它通过在标签中src属性引入对应的接口链接从而实现跨域请求。

    这里我借用掘金@浪里行舟 九种跨域方式实现原理中的JSONP实现代码展示:

    // index.html
    function jsonp({ url, params, callback }) {
      return new Promise((resolve, reject) => {
        let script = document.createElement('script')
        window[callback] = function(data) {
          resolve(data)
          document.body.removeChild(script)
        }
        params = { ...params, callback } // wd=b&callback=show
        let arrs = []
        for (let key in params) {
          arrs.push(`${key}=${params[key]}`)
        }
        script.src = `${url}?${arrs.join('&')}`
        document.body.appendChild(script)
      })
    }
    jsonp({
      url: 'http://localhost:3000/say',
      params: { wd: 'Iloveyou' },
      callback: 'show'
    }).then(data => {
      console.log(data)
    })
       
    

    我们可以看到,传入的url:http://localhost:3000/say 会走到函数中的script标签的src属性中,从而调用document.body.appendChild(script) 去跨域请求。通过JSONP实现了跨域。

    同样,如果你不想自己实现jsonp可以引用另一种方式,通过npm install Jsonp ,

    在页面中引用,具体代码如下:

    jsonp('http://localhost:3000/jsonp', null, (err, data) => {
        if (err) {
            console.error(err.message);
        } else {
            console.log(data)
        }
    });
    

    以及第三种方式,jQuery自带的jsonp方式,具体代码如下:

    $.ajax({
    	url: "http://localhost:9090/student",
        type: "GET", //jsonp只支持get请求
        dataType: "jsonp", //指定服务器返回的数据类型
        jsonp: "theFunction",   //指定参数名称,默认callback
        jsonpCallback: "showData",  //指定回调函数名称
        success: function (data) {
            var result = JSON.stringify(data); //json对象转成字符串
            $("#text").val(result);
        }
    });
    
  2. CORS

    CORS 指的是跨域资源共享,它需要双方的支持,基础是后端同学在服务端先支持跨域。那么具体是个怎样支持的过程呢?下面展示在Koa下服务端支持跨域的方式。

     const router = require('koa-router')();
     const send = require('koa-send');
    
     router
         .all('/*', async (ctx, next)=> {
             // 设置 Access-Control-Allow-Origin 支持跨域 * 标识支持全部ip跨域,此处也可以使用 ip 地址
             ctx.set('Access-Control-Allow-Origin', '*');
             ctx.set('Content-Type', 'application/json;charset=utf-8');
             ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS');
             ctx.set('Access-Control-Allow-Headers', 'content-type');
             ctx.set('Access-Control-Allow-Credentials', 'true');
             // 遇到options请求返回200
             if (ctx.request.method === 'OPTIONS') {
                 ctx.status = 200;
             } else {
                 await next();
             }
         })
         .get('/', (ctx, next) => {
             ctx.body = {data: 'get request from /!'};
         });
    

    通过设置 Access-Control-Allow-Origin 支持跨域,客户端可以不做任何处理,既可以实现跨域。开启后服务端接收到的请求将分为两种:简单请求与复杂请求

    简单请求

    简单请求的标志是请求方法限定为 head post get 方法之间的一种,头请求信息不超过以下几种字段:

     Accept
     Accept-Language
     Content-Language
     Last-Event-ID
     Content-Type:
         application/x-www-form-urlencoded、 multipart/form-data、text/plain
    

    则这类请求称之为简单请求。

    复杂请求

    复杂请求的标识是,浏览器会代理你发出一个option请求进行校验,他们头请求信息最常见的就是content-type=applicaiton/json,传参的方式自然就是Json串,下面展示一次复杂请求:

     $.ajax( 
         {
             url: 'http://127.0.0.1:3000/post',
             type: 'post',
             data: {
                 a: 1
             },
             dataType: 'json',
             contentType: 'applicaiton/json',
             success: function(res) {
                 console.log(res);
             },
             complete: function(xhr, ts) {
                 console.log('Status Code', xhr.status);
             }
         }
     );
    

    而OPTIONS请求一般会带三个关键请求头:

     Access-Control-Allow-Headers
     Access-Control-Allow-Methods
     Access-Control-Allow-Origin
    

    当校验成功的时候,就会发送真正的请求了。

    通过CORS进行跨域是一个极好的解决方案,在服务端开启跨域之后,客户端可以不做任何处理,浏览器会自行进行处理,对于请求方是无感知的(options请求)。

    Tips: 正常情况下,服务端开启跨域支持后,如果你的的浏览器依旧显示不能跨域,可尝试使用浏览器端的CORS插件再进行尝试。

  3. WebSocket

    WebSocket 是HTML5的一个基于TCP的持久化协议(HTTP也是基于TCP的),它实现了浏览器的全双工通信,它的具体工作流程是这样的,通过HTTP与服务端建立连接,建立连接之后,服务端与客户端之间既可以实现双向信息传输,客户端可以主动向服务端发送信息,服务端也可以主动跟客户端交互,而这之间的交互就是基于WebSocket的。以下举个koa中实现WebScoket的例子,附代码。

     // 服务端代码
     const Koa = require('koa');
     const app = new Koa();
    
     // 挂载中间件可以在这里挂载
     // ...
    
     // websocket 将已挂载中间件的 app 打入 server
     const server = require('http').createServer(app.callback());
     const io = require('socket.io')(server);
    
     server.listen(3000, () => {
         console.log(`app run at : http://127.0.0.1:3000`);
     })
    
     // websocket 开启监听
     io.on('connection', socket => {
         socket.on('send', data => {
             console.log('客户端发送的内容:', data);
             socket.emit('getMsg', '返回消息: 我接收到了' + data);
         });
         console.log('websocket初始化成功');
         socket.emit('getMsg', 'websocket初始化成功...');
     });
    
     // 客户端代码 通过http-server开启一个服务来打开页面
     <!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">
         <meta http-equiv="Cache-Control" content="no-cache">
         <!-- 此处的地址为服务端地址下的socket文件,我的服务器为127.0.0.1:3000 -->
         <script src="http://127.0.0.1:3000/socket.io/socket.io.js" ></script>
         <title>Document</title>
     </head>
     <body>
         <button id="send" onclick="emitFun()">发送到服务器</button>
     </body>
     <script>
         var socket = io('ws://127.0.0.1:3000');
         var send = document.querySelector('#send');
         var msg = document.querySelector('#msg');
    
         socket.on('getMsg', data => {
         console.log('服务端消息:',  data);
         })
    
         function emitFun() {
             console.log('点击了发送消息!');
         // 通过emit触发发送请求
         socket.emit('send', 'hello');
         }
     </script>
     </html>
    

    效果:

    跨域的方式有很多种,以上只展示常见的三种,也是我自己比较熟悉的三种跨域方式,跨域在我们的前后端分离的大环境下,已经变成家常便饭一样的存在,不能好好处理跨域问题的程序员不是一个好程序员。

参考文献/博客

  1. 跨域的几种方式 http://www.imooc.com/article/40074
  2. 九种跨域方式实现原理 https://juejin.im/post/5c23993de51d457b8c1f4ee1
  3. socket.io 官网 https://socket.io/

至此,希望此博客能对你有所帮助。