Profession & Efficient

保持对技术的敬畏之心

「bug信息收集系统」进阶及源码解析

内容简介

《「bug信息收集系统」进阶及源码解析》

上一章我们介绍了「bug信息收集系统」的搭建和基本使用,本章将介绍它的高级用法,并对源码进行解析。主要分为以下三个部分:

  • 数据安全问题,如何保证上报数据的安全性?
  • front-tool的扩展功能
  • front-tool的源码解析
  • 由于「前端报错信息收集」功能还在开发中,这里暂不作介绍

数据安全问题(leanCould中的权限管理)

问题所在

我们先来明确目前遇到的问题

  • 数据不安全是由于leanCloud的配置信息(APP ID,APP KEY)是通过明文保存到前端代码中的。
  • 即使代码进行了混淆,但使用过leanCloud的同学还是可以很容易从中找到配置信息。
  • 获取配置信息后,通过leanCloud提供的JS方法就可以获取上报的数据内容。造成用户信息的泄漏。
    leanCloud的配置信息是不可避免会被泄漏的(即使通过后端返回,黑客也可以查看接口拿到配置信息),那我们怎么在配置信息不安全的情况下,仍保持数据的安全性,不让黑客获取我们上报的数据呢?

    《「bug信息收集系统」进阶及源码解析》

解决方法

我们可以通过leanCould的访问控制列表(ACL 机制)(Access Control List)来实现
其原理通过在leanCloud中设置不同的角色,并为不同的角色设置不同的权限,来约束其行为。具体流程如下:

  • 设置两个角色,role_write,role_read。
  • 为role_write角色仅分配写权限(再明确地说,是新增数据项的权限)。
  • role_read角色分配读权限,及删除权限。
  • 在front-tool组件中使用role_write角色登录。
  • 在Bug的管理系统(bug-list,bug-detail)中使用role_read角色登录。
    这样即使被黑客获取了role_write角色的配置信息,也仅能添加新数据,不能修改,更不能查看。
    而role_read角色是对于内部人员使用的,没有信息安全问题。从而解决数据安全问题。

    《「bug信息收集系统」进阶及源码解析》

具体配置方法如下

第一步,调用leanCloud方法添加两个用户角色。

  • 注意:这里需要将角色的账号密码自行记录起来,因为密码是不会明文保存到leanCloud上的,要找回来比较麻烦,自行保存比较好。
  • 角色注册完成后,进入之前创建的应用,找到系统默认创建的_User类,点击
  • 右侧展示的,就是我们刚创建的两个角色
    <script src="https://cdn1.lncld.net/static/js/av-min-1.2.1.js"></script>
    <script>
    AV.init({
    appId: 'xxx',
    appKey: 'xxx',
    });
    
    // 注册角色
    function signup() {
    var user = new AV.User();
    user.setUsername('role_write');
    user.setPassword('xxx');
    user.signUp().then(function (loggedInUser) {
      alert('注册成功')
      console.log(loggedInUser);
    });
    }
    </script>
    

第二步,关闭leanCloud使用js添加角色的权限

  • 不知道大家发现没有,这上面有一个坑,创建角色仅仅使用配置信息即可完成。
  • 黑客拿到配置信息后,创建新角色,就可以绕过或破坏(如删除角色)我们的角色权限机制。
  • 所以这里需要在leanCloud后台修改配置,禁止通过JS进行角色的相关操作。
  • 同样找到_User,点击其他,找到「权限设置」选项,将里面所有的权限都设置「指定用户」,即没有角色对其进行修改。
  • 这样就可关闭_User类的操作功能,通过JS代码无法对其进行修改。

    《「bug信息收集系统」进阶及源码解析》

第三步,设置bug类的访问权限

  • 找到bug类,打开「权限设置」,为里面不同的权限分配到不同角色上。
  • 把add_fields(新建字段),create(新建数据),指定用户到role_write上,即只有该角色可以做新增的操作
  • 为delete(删除数据),find(搜索),get(根据id查询),指定用户到role_read上,即只有该角色可以做查询删除操作
  • update(修改数据项)置空

    《「bug信息收集系统」进阶及源码解析》

第四步,在vue组件中,添加登录语句。

  • 在front-tool组件中使用role_write角色登录。
  • 在Bug的管理系统(bug-list,bug-detail)中使用role_read角色登录。
  • 方法很简单,在初始化后面加一条登录语句即可
   AV.init({
    appId: 'xxx',
    appKey: 'xxx',
  })
  AV.User.logIn('role_write', 'xxx')

front-tool功能扩展

接口检测功能

  • 组件可配置一个接口检测函数ajaxHook,利用该函数可以对所有接口返回的数据进行检查。
  • ajaxHook接受一个ajaxData参数,里面记录了当前接口的信息,如request,response,header等。可用于检查接口状态,对接口进行断言。
  • 当接口返回的状态码不正确时,在函数中返回true,即可触发数据上报功能。
  • 如果希望在生产环境也使用接口检测功能,自动上报数据,需要将front-tool组件的useInProd参数设置为true。
  • 该函数的意义在于检测生产环境的接口报错,做错误监控。
  • 开发同学可以每天上班前到Bug信息收集系统中通过URL检索生产环境的bug信息,检测线上问题。

    《「bug信息收集系统」进阶及源码解析》

添加自定义菜单

  • 在front-tool组件中,我们可以添加一些自定义的方法
  • 实现某些便捷的功能,方便测试同学使用的方法,提高效率,如「清除token,重新登录」,「清除缓存信息等」
  • 使用方法进行菜单配置即可,如

    《「bug信息收集系统」进阶及源码解析》

front-tool源码解析

运行环境(域名环境)检测

  • 原理很简单,检测当前页面URL是否匹配特定的域名前缀。
  // 获取运行环境
  getRuntimeEnv() {
    let envObj = {
      dev: 'xxx',
      test: 'xxx',
      pre: 'xxx',
      prod: 'xxx',
    }
    for (let key in envObj) {
      if (location.href.indexOf(envObj[key]) !== -1) {
        return key
      }
    }
    return 'local'
  }

全局方法注入及注销

  • 组件创建时,会先获取当前域名环境,并检测useInProd参数,如果是生产环境且useInProd不为真。则将全局暴露的函数全部设置为空函数。
  • 否则进入初始化函数,覆写ajax功能,往ajax相关函数中加入我们想要的代码。
  • 注册各个全局函数
  • 其中AV是leanCloud定义的全局变量,获取AV._config.applicationId,用以判断leanCloud是否被初始化过。
  • 做这个检测,是因为在vue项目开发中,热编译会致使部分代码会被重新加载。导致leanCloud多次加载而报错。故需要在这里先检测是否已经被加载过,再去进行初始化操作。

    init() {
     this.resetAjax() // 覆写ajax
     this.$root.__proto__.$addCustomData = this.addCustomData.bind(this) // 注册全局函数
     this.$root.__proto__.$clearCustomData = this.clearCustomData.bind(this)
     this.$root.__proto__.$addGlobalData = this.addGlobalData.bind(this)
     this.$root.__proto__.$clearGlobalData = this.clearGlobalData.bind(this)
     window.$collectData = this.reportDate.bind(this)
     if (!AV._config.applicationId) { // 检测AV是否已经被初始化过
       AV.init({
         appId: 'I7QMGWueNtd27ILAiMQqUAzI-gzGzoHsz',
         appKey: 'WRoSIQ8hSGLq9xLbaWDe9f7y',
       })
       AV.User.logIn('XtRuRZtca-for-add', 'Xw8XFNAidgEbJefXnCuG')
     }
    }
    

ajax覆写,实现接口监听

  • 同样的,我们通过window._hadResetAjax变量标识ajax是否已经被复写过,防止多次运行,多次覆写。
  • ajax覆写的原理,
    • 是先将ajax原本的函数通过变量保存,
    • 覆写ajax的函数,
    • 在覆写的函数中加入我们的代码,并调用前面保存的ajax原始函数,
    • 相当于在ajax函数中添加hook。和vue的生命周期概念类似,运行到某环节时,执行某操作。
  • 需要注意的是,ajax的相关函数是定义在其原型链上的,在保存其原始函数时,需要保存原型链上的函数

  • 首先通过变量保存原始的window.XMLHttpRequest,及其原型链上的open,send,setRequestHeader方法

    let originXHR = window.XMLHttpRequest
    let originOpen = originXHR.prototype.open
    let originSend = originXHR.prototype.send
    let originSetRequestHeader = originXHR.prototype.setRequestHeader
    
  • 覆写 window.XMLHttpRequest 构造函数
    • 在新的构造函数内部生成原始window.XMLHttpRequest对象的实例
    • 复写open,send,setRequestHeader方法。复写方法是让其重新赋值为一个新的函数,并在该函数中添加我们的代码。在最后调用一开始保存的,原始的window.XMLHttpRequest中对应的方法
    • 调用原始方法时,需要通过call方法修改this的指向,让其指到window.XMLHttpRequest实例。
    • 在覆写函数时,需要将接收的函数参数原封不动地传会给原始的方法中,不能影响和改变函数的使用方式。这也是为什么不影响axios的使用,因为我们覆写的是ajax的底层实现。axios是在其内容上进行二次开发的,所以加载本组件不会对axios有任何影响
    • 原则上,可以通过这种方式对ajax进行任意次的覆写
  • 这里介绍一下每个覆写函数的中做了什么操作
    • 覆写open,用于收集请求方式,请求链接,请求的query参数。
    • 覆写setRequestHeader,用于收集请求的头信息,以便收集如token等验证信息。
    • 覆写send,用于手机post请求的参数,timeout,responseType等信息。
  • 最后是监听接口loadend事件,即接口响应事件
    • 记录接口返回的内容。
    • 进行接口hook操作,调用父组件传入的ajaxHook函数,并在函数中传递本次接口调用的相关信息。
    • 当ajaxHook返回true时,调用数据上报函数,上报数据。
    • 这里做了一个优化,对请求的链接进行检测,当接口是leanCloud的接口时,不进行数据记录以及ajaxHook。
  • 如果大家对JS的继承有印象的话,会发现,这个做法与JS的实例继承类似,在内部生产父类的实例,对实例进行一系列操作后,返回这个实例
    // 重写AJAX
    resetAjax() {
    if (window._hadResetAjax) { // 如果已经重置过,则不再进入。解决开发时局部刷新导致重新加载问题
      return
    }
    window._hadResetAjax = true
    let originXHR = window.XMLHttpRequest
    let originOpen = originXHR.prototype.open
    let originSend = originXHR.prototype.send
    let originSetRequestHeader = originXHR.prototype.setRequestHeader
    
    // 重置事件
    window.XMLHttpRequest = () => {
      let ajaxData = {} // 整个ajax数据,收集数据时用
      let realXHR = new originXHR()
    
      // 重置操作函数,获取请求数据
      realXHR.open = (method, url, asyn) => {
        ajaxData.request = {
          method: method,
          url: url.split('?')[0],
          data: this.getParams(url),
          header: {},
        }
        originOpen.call(realXHR, method, url, asyn)
      }
    
      // 重置设置请求头的函数
      realXHR.setRequestHeader = (header, value) => {
        ajaxData.request.header[header] = value
        originSetRequestHeader.call(realXHR, header, value)
      }
    
      // 重置操作函数,获取请求数据
      realXHR.send = (postData) => {
        ajaxData.request.timeout = realXHR.timeout
        ajaxData.request.responseType = realXHR.responseType
        if (postData) {
          ajaxData.request.data = typeof postData === 'string' ? this.getParams(`?${postData}`) : postData
        }
        try { // 防止timeout等报错,造成程序阻塞
          originSend.call(realXHR, postData)
        } catch (e) {
          console.log(e)
        }
      }
    
       // 监听加载完成,获取回复的报文
       realXHR.addEventListener('loadend', () => {
         ajaxData.response = realXHR.response
         if (ajaxData.request.url.indexOf('api.leancloud.cn') === -1) {
           this.ajaxList.push(ajaxData)
           if (this.ajaxHook) { // 外部执行钩子
             this.ajaxHook(ajaxData) && this.reportDate()
           }
         }
       }, false)
       return realXHR
    }
    },
    
    

讲完了,我要回去搬砖了,88

《「bug信息收集系统」进阶及源码解析》

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注