美文网首页
「深入浅出」前端开发中常用的几种跨域解决方案

「深入浅出」前端开发中常用的几种跨域解决方案

作者: LAXlerbo | 来源:发表于2020-09-21 14:07 被阅读0次
    image

    编者荐语

    本文将为大家介绍,前端开发中,最常用的几种跨域解决方案;

    看完本文可以系统地掌握,不同种跨域解决方案间的巧妙,以及它们的用法、原理、局限性和适用的场景

    包括以下几个方面:

    • 跨域的现象,和几种常见的跨域表现
    • 跨域的解决方案(原理分析)
      • 修改本地HOST
      • JSONP
      • CORS
      • Proxy
      • Nginx反向代理
      • Post Message(利用iframe标签,实现不同域的关联)
    • 几种解决方案的比较和适用场景

    如果你还是对跨域概念或单单配置还是很模糊的话,或者面试中经常被问到,但是回答的不全面的话,可能这篇文章适合你。

    公众号👉 DevOS,查看更多更质量文章。

    同源是什么?

    如果两个URL的协议protocol、主机名host和端口号port都相同的话,则这两个URL是同源。

    同源策略

    同源策略是一个重要的安全策略。它能够阻断恶意文档,减少被攻击的媒介。

    真实项目中,很少有同源策略,大部分都是非同源策略

    跨域是什么?

    当协议、域名与端口号中任意一个不相同时,都算作不同域,不同域之间相互请求资源的表现(非同源策略请求),称作”跨域“

    跨域现象

    那么我们就下面的网址分析一下,哪一块是协议,哪一块是域名及端口号

    http://kbs.sports.qq.com/index.html
    
    协议:http(还有以一种https协议)
    域名:kbs.sports.qq.com
    端口号:80
    
    https://127.0.0.1:3000
    
    协议:https
    域名:127.0.0.1
    端口号:3000
    复制代码
    

    假如我们的真实项目开发中的Web服务器地址为 ”kbs.sports.qq.com/index.html“… "api.sports.qq.com/list"

    当Web服务器的地址向数据接口的地址发送请求时,便会造成了跨域现象。

    造成跨域的几种常见表现

    • 服务器分开部署(Web服务器 + 数据请求服务器)
    • 本地开发(本地预览项目 调取 测试服务器的数据)
    • 调取第三方平台的接口

    Web服务器:主要用来静态资源文件的处理

    解决方案

    • 修改本地HOST(不作介绍)
    • JSONP
    • CORS
    • Proxy
    • Nginx反向代理
    • Post Message(利用iframe标签,实现不同域的关联)

    在后面会详细分析这四种解决方案的原理和用法配置,以及它们的优点和局限性

    注意: 基于ajaxfetch发送请求时,如果是跨域的,则浏览器默认的安全策略会禁止该跨域请求

    补充说明:以下所有的测试用例,均由Web:http://127.0.0.1:5500/index.htmlAPI:http://127.0.0.1:1001/list发起请求

    API接口的服务器端是自己通过express建立的,下文在服务器端以app.use中间件的形式接受来自客户端的请求并做处理。

    即 在“http://127.0.0.1:1001/list”from origin“http://127.0.0.1:55”上对XMLHttpRequest的访问已被CORS策略阻止:被请求的资源上没有“Access- control - allow-origin”头

    在后端开启了一个端口号为1001的服务器之后,我们来实践一下

        let xhr = new XMLHttpRequest;
        xhr.open('get', 'http://127.0.0.1:1001/list');
        xhr.onreadystatechange = () => {
          if (xhr.status === 200 && xhr.readyState === 4) {
            console.log(xhr.responseText);
          }
        };
        xhr.send(); 
    
    跨域的常见报错提示

    这就是由于浏览器默认的安全策略禁止导致的。

    下面介绍一下几种常见的解决方案。

    JSONP

    原理:JSONP利用script标签不存在域的限制,且定义一个全局执行上下文中的函数func

    (用来接收服务器端返回的数据信息)来接收数据,从而实现跨域请求。

    弊端

    • 只允许GET请求
    • 不安全:只要浏览器支持,且存在浏览器的全局变量里,则谁都可以调用

    图解JSONP的原理

    image

    手动封装JSONP

    callback必须是一个全局上下文中的函数

    (防止不是全局的函数,我们需要把这个函数放在全局上,并且从服务器端接收回信息时,要浏览器执行该函数)

    注意:

    • uniqueName变量存储全局的回调函数(确保每次的callback都具有唯一性)
    • 检验url中是否含有"?",有的话直接拼接callback,没有的话补”?“
        // 客户端
        function jsonp(url, callback) {
          // 把传递的回调函数挂载到全局上
          let uniqueName = `jsonp${new Date().getTime()}`;
          // 套了一层 anonymous function
          // 目的让 返回的callback执行且删除创建的标签
          window[uniqueName] = data => {
            // 从服务器获取结果并让浏览器执行callback
            document.body.removeChild(script);
            delete window[uniqueName];
            callback && callback(data);
          }
          
          // 处理URL
          url += `${url.includes('?')} ? '&' : '?}callback=${uniqueName}'`;
          
          // 发送请求
          let script = document.createElement('script');
          script.src = url;
          document.body.appendChild(script);
        }
        
        // 执行第二个参数 callback,获取数据
        jsonp('http://127.0.0.1:1001/list?userName="lsh"', (result) => {
          console.log(result);
        })
      
    
        // 服务器端
        // Api请求数据
        app.get('/list', (req, res) => {
          // req.query 问号后面传递的参数信息
          // 此时的callback 为传递过来的函数名字 (uniqueName)
          let { callback } = req.query;
          
          // 准备返回的数据(字符串)
          let res = { code: 0, data: [10,20] };
          let str = `${callback}($(JSON.stringify(res)))`;
          
          // 返回给客户端数据
          res.send(str);
        })
    

    测试用例展示:

    • 客户端请求的url
    • 服务端返回的数据
      • 返回的callback
      • 返回的数据信息 result
        // 服务器请求的 url
        Request URL:
          http://127.0.0.1:1001/list?userName="lsh"&callback=jsonp159876002
        
        // 服务器返回的函数callback
         jsonp159876002({"code":0, "data": [10,20]});
        
        // 客户端接收的数据信息
        { code: 0, data: Array(2) }
    

    当浏览器发现返回的是 jsonp159876002({"code":0, "data": [10,20]});这个函数,会自动帮我们执行的。

    JSONP弊端

    在上文中说到只要服务器端那里设置了允许通过jsonp的形式跨域请求,我们就可以取回数据。

    下面是在我们封装完jsonp方法之后,向一个允许任何源向该服务器发送请求的网址xxx

        jsonp('https://matchweb.sports.qq.com/matchUnion/cateColumns?from=pc', result => {
          console.log(result);
        });
    
    image

    CORS

    上文提到,不允许跨域的根本原因是因为Access-Control-Allow-Origin已被禁止

    那么只要让服务器端设置允许源就可以了

    原理:解决掉浏览器的默认安全策略,在服务器端设置允许哪些源请求就可以了

    先来看一下下面的设置有哪些问题

        // 服务器端
        app.use((req, res, next) => {
          // * 允许所有源(不安全/不能携带资源凭证)
          res.header("Access-Control-Allow-Origin", "*");
          res.header("Access-Control-Allow-Credentials", true);
        
          /* res.header("Access-Control-Allow-Headers", "Content-Type,....");
          res.header("Access-Control-Allow-Methods", "GET,..."); */
        
          // 试探请求:在CORS跨域请求中,首先浏览器会自己发送一个试探请求,验证是否可以和服务器跨域通信,服务器返回200,则浏览器继续发送真实的请求
          req.method === 'OPTIONS' ? res.send('CURRENT SERVICES SUPPORT CROSS DOMAIN REQUESTS!') : next();
        });
        
        // 客户端
        let xhr = new XMLHttpRequest;
        xhr.open('get', 'http://127.0.0.1:1001/list');
        xhr.setRequestHeader('Cookie', 'name=Jason');
        xhr.withCredentials = true;
        xhr.onreadystatechange = () => {
          if (xhr.status === 200 && xhr.readyState === 4) {
            console.log(xhr.responseText);
          }
        };
        xhr.send();
    

    当我们一旦在服务器端设置了允许任何源可以请求之后,其实请求是不安全的,并且要求客户端不能携带资源凭证(比如上文中的Cookie字段),浏览器端会报错。

    告诉我们Cookie字段是不安全的也不能被设置的,如果允许源为'*'的话也是不允许的。

    image

    假如在我们的真实项目开发中

    正确写法✅

    • 设置单一源(安全/也可以携带资源凭证/只能是单一一个源)
    • 也可以动态设置多个源:每一次请求都会走这个中间件,我们首先设置一个白名单,如果当前客户端请求的源在白名单中,我们把Allow-Origin动态设置为当前这个源
        app.use((req, res, next) => {
          
          // 也可自定义白名单,检验请求的源是否在白名单里,动态设置
          /* let safeList = [
              "http://127.0.0.1:5500",
              xxx,
              xxxxx,
          ]; */
          res.header("Access-Control-Allow-Origin", "http://127.0.0.1:5500");
          res.header("Access-Control-Allow-Credentials", true); // 设置是否可携带资源凭证
        
          /* res.header("Access-Control-Allow-Headers", "Content-Type,....");
          res.header("Access-Control-Allow-Methods", "GET,..."); */
        
          // 试探请求:在CORS跨域请求中,首先浏览器会自己发送一个试探请求,验证是否可以和服务器跨域通信,服务器返回200,则浏览器继续发送真实的请求
          req.method === 'OPTIONS' ? res.send('CURRENT SERVICES SUPPORT CROSS DOMAIN REQUESTS!') : next();
        });
    

    CORS的好处

    • 原理简单,容易配置,允许携带资源凭证
    • 仍可以用 ajax作为资源请求的方式
    • 可以动态设置多个源,通过判断,将Allow-Origin设置为当前源

    CORS的局限性

    • 只允许某一个源发起请求
    • 如多个源,还需要动态判断

    Proxy

    Proxy翻译为“代理”,是由webpack配置的一个插件,叫"webpack-dev-server"(只能在开发环境中使用)

    Proxy在webpack中的配置

        const path = require('path');
        const HtmlWebpackPlugin = require('html-webpack-plugin');
        
        module.exports = {
          mode: 'production',
          entry: './src/main.js',
          output: {...},
          devServer: {
            port: '3000',
            compress: true,
            open: true,
            hot: true,
            proxy: {
              '/': {
                target: 'http://127.0.0.1:3001',
                changeOrigin: true
              }
            }
          },
          // 配置WEBPACK的插件
          plugins: [
            new HtmlWebpackPlugin({
              template: `./public/index.html`,
              filename: `index.html`
            })
          ]
        };
    

    图解Proxy的原理

    image

    Proxy代理其实相当于由webpack-dev-server配置在本地创建了一个port=3000的服务,利用node的中间层代理(分发)解决了浏览器的同源策略限制。

    但是它只能在开发环境下使用,因为dev-server只是一个webpack的一个插件;

    如果需要在生产环境下使用,需要我们配置Nginx反向代理服务器;

    另外如果是自己实现node服务层代理:无论是开发环境还是生产环境都可以处理(node中间层和客户端是同源,中间层帮助我们向服务器请求数据,再把数据返回给客户端)

    Proxy的局限性

    只能在本地开发阶段使用

    配置Nginx反向代理

    主要作为生产环境下跨域的解决方案。

    原理:利用Node中间层的分发机制,将请求的URL转向服务器端的地址

    配置反向代理

        server {
          listen: 80;
          server_name: 192.168.161.189;
          loaction: {
            proxy_pass_http://127.0.0.1:1001; // 请求转向这个URL地址,服务器地址
            root html;
            index index.html;
          }
        }
      
    

    简单写了一下伪代码,实际开发中根据需求来配。

    POST MESSAGE

    假设现在有两个页面,分别为A页面port=1001、B页面port=1002,实现页面A与页面B的页面通信(跨域)

    原理:

    • 把 B页面当做A的子页面嵌入到A页面里,通过iframe.contentWindow.postMessage向B页面传递某些信息
    • 在A页面中通过 window.onmessage获取A页面传递过来的信息ev.data(见下代码)
    • 同理在B页面中通过ev.source.postMessage向A页面传递信息
    • 在A页面中通过window.onmessage获取B页面传递的信息

    主要利用内置的postMessageonmessage传递信息和接收信息。

    A.html

        // 把 B页面当做A的子页面嵌入到A页面里
        <iframe id="iframe" src="http://127.0.0.1:1002/B.html" frameborder="0" style="display: none;"></iframe>
        
        <script>
          iframe.onload = function () {
            iframe.contentWindow.postMessage('珠峰培训', 'http://127.0.0.1:1002/');
          }
        
          //=>监听B传递的信息
          window.onmessage = function (ev) {
            console.log(ev.data);
          }
        </script>
    

    B.html

        <script>
          //=>监听A发送过来的信息
          window.onmessage = function (ev) {
            // console.log(ev.data);
        
            //=>ev.source:A
            ev.source.postMessage(ev.data + '@@@', '*');
          }
        </script>
    

    几种方案的比较

    1. JSONP方案需要前后端共同配置完成(利用script标签不存在域的限制)【麻烦,老项目使用】

    2. CORS原理简单,但只能配置单一源,如果需要配置多个源,也只能从白名单中筛选出某一个符合表求的origin【偶尔使用】

    服务器端需要单独做处理,客户端较为简单

    3. Proxy客户端通过dev-server,生产环境需要配置Nginx反向代理(利用Node中间层分发机制)【常用】

    感谢大家

    如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

    1. 点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)
    2. 关注公众号「DevOS」,持续为你推送精选好文
    3. 觉得不错的话,也可以阅读时光屋小豪近期梳理的文章(感谢大家的鼓励与支持🌹🌹🌹)

    相关文章

      网友评论

          本文标题:「深入浅出」前端开发中常用的几种跨域解决方案

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