「bug信息收集系统」进阶及源码解析
内容简介
上一章我们介绍了「bug信息收集系统」的搭建和基本使用,本章将介绍它的高级用法,并对源码进行解析。主要分为以下三个部分:
- 数据安全问题,如何保证上报数据的安全性?
- front-tool的扩展功能
- front-tool的源码解析
- 由于「前端报错信息收集」功能还在开发中,这里暂不作介绍
数据安全问题(leanCould中的权限管理)
问题所在
我们先来明确目前遇到的问题
- 数据不安全是由于leanCloud的配置信息(APP ID,APP KEY)是通过明文保存到前端代码中的。
- 即使代码进行了混淆,但使用过leanCloud的同学还是可以很容易从中找到配置信息。
- 获取配置信息后,通过leanCloud提供的JS方法就可以获取上报的数据内容。造成用户信息的泄漏。
leanCloud的配置信息是不可避免会被泄漏的(即使通过后端返回,黑客也可以查看接口拿到配置信息),那我们怎么在配置信息不安全的情况下,仍保持数据的安全性,不让黑客获取我们上报的数据呢?
解决方法
我们可以通过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角色是对于内部人员使用的,没有信息安全问题。从而解决数据安全问题。
具体配置方法如下
第一步,调用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类,打开「权限设置」,为里面不同的权限分配到不同角色上。
- 把add_fields(新建字段),create(新建数据),指定用户到role_write上,即只有该角色可以做新增的操作
- 为delete(删除数据),find(搜索),get(根据id查询),指定用户到role_read上,即只有该角色可以做查询删除操作
- update(修改数据项)置空
第四步,在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信息,检测线上问题。
添加自定义菜单
- 在front-tool组件中,我们可以添加一些自定义的方法
- 实现某些便捷的功能,方便测试同学使用的方法,提高效率,如「清除token,重新登录」,「清除缓存信息等」
- 使用方法进行菜单配置即可,如
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