Vue实现按钮级权限方案


Posted in Javascript onNovember 21, 2019

演示

在年初开发一个中后台管理系统,功能涉及到了各个部门(产品、客服、市场等等),在开始的版本中,我和后端配合使用了花裤衩手摸手系列的权限方案,前期非常nice,但是慢慢的随着功能增多、业务越来越复杂,就变得有些吃力了,因为我们的权限动态性太大了

  • 手摸手系列权限方案是有比较清晰的权限划分的,而我们公司部门的岗位职责有时比较模糊。
  • 后端采用RBAC权限方案,为了达到第1点要求,将角色划分的很细,并且角色有时频繁变动,导致每一次前端都需要手动维护
  • 为了解决上面2个痛点,我将原方案进行了一丢丢改造。
  • 前端不再以角色来控制权限,而是以更小粒度的操作(接口)来控制,也就是前端不关心角色
  • 路由还是由前端维护(我们的后端很排斥维护和他们不相干的东西:joy:),但改为通过操作列表对权限路由进行过滤
  • 使用单一的方式(方便维护)控制页面的局部权限,不再使用自定义指令方式,而是通过函数式组件,原因是使用自定义指令有多余的开销(插入再移除)

后端的配合:

routerName

有一些注意点:

  • 比如一个有权限的列表页面A,同时这个列表接口被权限页面B使用,现在你配置权限让某一个用户没有A页面权限,但可以使用B页面,如果你的本意是可以使用B页面的所有功能,这时就会有问题,所以尽量不要将权限接口跨页面使用,需要分清哪些数据需要通过字典接口获取还是通过权限接口获取
  • 有些人可能会纠结,前端维护权限安全吗?肯定是不安全的,安全性主要还在后端这边把控,后端做好数据和接口方面的权限控制,前端做权限控制我认为主要还是为了交互体验等。没有权限你为什么要让我看到那一坨?
  • 在使用这种方式之前,要明确当前场景是否确实需要这么做,毕竟在项目比较大且接口很多的情况下,你跟操作码之间有一场持久战

实现

操作列表示例

以Restful风格接口为例

const operations = [
 {
 url: '/xxx',
 type: 'get',
 name: '查询xxx',
 routeName: 'route1', // 接口对应的路由
 opcode: 'XXX_GET' // 操作码,不变的
 },
 {
 url: '/xxx',
 type: 'post',
 name: '新增xxx',
 routeName: 'route1',
 opcode: 'XXX_POST'
 },
 // ......
]

路由的变化

在路由的 meta 中增加一个配置字段如 requireOps ,值可能为 String 或者 Array ,这表示当前路由页面要显示的必要的操作码, Array 类型是为了处理一个路由页面需要满足同时存在多个操作权限时才显示的情况。若值不为这2种则视为无权限控制,任何用户都能访问

由于最终需要根据过滤后的权限路由动态生成菜单,所以还需要在路由选项中增加几个字段处理显示问题,其中 hidden 优先级大于 visible

hidden
visible
const permissionRoutes = [
 {
 // visible: false,
 // hidden: true,
 path: '/xxx',
 name: 'route1',
 meta: {
  title: '路由1',
  requireOps: 'XXX_GET'
 },
 // ...
 }
]

由于路由在前端维护,所以以上配置只能写死,如果后端能同意维护这一份路由表,那就可以有很多的发挥空间了,体验也能做的更好。

权限路由过滤

先将权限路由规范一下,同时保留一个副本,可能在可视化时需要用到

const routeMap = (routes, cb) => routes.map(route => {
 if (route.children && route.children.length > 0) {
 route.children = routeMap(route.children, cb)
 }
 return cb(route)
})
const hasRequireOps = ops => Array.isArray(ops) || typeof ops === 'string'
const normalizeRequireOps = ops => hasRequireOps(ops)
 ? [].concat(...[ops])
 : null
const normalizeRouteMeta = route => {
 const meta = route.meta = {
 ...(route.meta || {})
 }
 meta.requireOps = normalizeRequireOps(meta.requireOps)
 return route
}

permissionRoutes = routeMap(permissionRoutes, normalizeRouteMeta)
const permissionRoutesCopy = JSON.parse(JSON.stringify(permissionRoutes))

获取到操作列表后,只需要遍历权限路由,然后查询 requireOps 代表的操作有没有在操作列表中。这里需要处理一下 requireOps 未设置的情况,如果子路由中都是权限路由,需要为父级路由自动加上 requireOps 值,不然当所有子路由都没有权限时,父级路由就被认为是无权限控制且可访问的;而如果子路由中只要有一个路由无权限控制,那就不需要处理父路由。所以这里可以用递归来解决,先处理子路由再处理父路由

const filterPermissionRoutes = (routes, cb) => {
 // 可能父路由没有设置requireOps 需要根据子路由确定父路由的requireOps
 routes.forEach(route => {
 if (route.children) {
  route.children = filterPermissionRoutes(route.children, cb)
  
  if (!route.meta.requireOps) {
  const hasNoPermission = route.children.some(child => child.meta.requireOps === null)
  // 如果子路由中存在不需要权限控制的路由,则跳过
  if (!hasNoPermission) {
   route.meta.requireOps = [].concat(...route.children.map(child => child.meta.requireOps))
  }
  }
 }
 })

 return cb(routes)
}

然后根据操作列表对权限路由进行过滤

let operations = null // 从后端获取后更新它
const hasOp = opcode => operations
 ? operations.some(op => op.opcode === opcode)
 : false

const proutes = filterPermissionRoutes(permissionRoutes, routes => routes.filter(route => {
 const requireOps = route.meta.requireOps

 if (requireOps) {
 return requireOps.some(hasOp)
 }

 return true
}))

// 动态添加路由
router.addRoutes(proutes)

函数式组件控制局部权限

这个组件实现很简单,根据传入的操作码进行权限判断,若通过则返回插槽内容,否则返回null。另外,为了统一风格,支持一下 root 属性,表示组件的根节点

const AccessControl = {
 functional: true,
 render (h, { data, children }) {
 const attrs = data.attrs || {}

 // 如果是root,直接透传
 if (attrs.root !== undefined) {
  return h(attrs.root || 'div', data, children)
 }

 if (!attrs.opcode) {
  return h('span', {
  style: {
   color: 'red',
   fontSize: '30px'
  }
  }, '请配置操作码')
 }

 const opcodes = attrs.opcode.split(',')

 if (opcodes.some(hasOp)) {
  return children
 }

 return null
 }
}

动态生成权限菜单

以ElementUI为例,由于动态渲染需要进行递归,如果以文件组件的形式会多一层根组件,所以这里直接用render function简单写一个示例,可以根据自己的需求改造

// 权限菜单组件
export const PermissionMenuTree = {
 name: 'MenuTree',
 props: {
 routes: {
  type: Array,
  required: true
 },
 collapse: Boolean
 },
 render (h) {
 const createMenuTree = (routes, parentPath = '') => routes.map(route => {
  // hidden: 为true时当前菜单和子菜单都不显示
  if (route.hidden === true) {
  return null
  }

  // 子路径处理
  const fullPath = route.path.charAt(0) === '/' ? route.path : `${parentPath}/${route.path}`

  // visible: 为false时不显示当前菜单,但显示子菜单
  if (route.visible === false) {
  return createMenuTree(route.children, fullPath)
  }

  const title = route.meta.title
  const props = {
  index: fullPath,
  key: route.path
  }

  if (!route.children || route.children.length === 0) {
  return h(
   'el-menu-item',
   { props },
   [h('span', title)]
  )
  }

  return h(
  'el-submenu',
  { props },
  [
   h('span', { slot: 'title' }, title),
   ...createMenuTree(route.children, fullPath)
  ]
  )
 })

 return h(
  'el-menu',
  {
  props: {
   collapse: this.collapse,
   router: true,
   defaultActive: this.$route.path
  }
  },
  createMenuTree(this.routes)
 )
 }
}

接口的权限控制

我们一般用axios,这里只需要在axios封装的基础上加几行代码就可以了,axios封装花样多多,这里简单示例

const ajax = axios.create(/* config */)

export default {
 post (url, data, opcode, config = {}) {
 if (opcode && !hasOp(opcode)) {
  return Promise.reject(new Error('没有操作权限'))
 }
 return ajax.post(url, data, { /* config */ ...config }).then(({ data }) => data)
 },
 // ...
}

到这里,这个方案差不多就完成了,权限配置的可视化可以根据操作列表中的 routeName 来做,将操作与权限路由一一对应,在 demo 中有一个简单实现

总结

以上所述是小编给大家介绍的Vue实现按钮级权限方案,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

Javascript 相关文章推荐
jquery 表格分页等操作实现代码(pagedown,pageup)
Apr 11 Javascript
javascript轻松实现当鼠标移开时已弹出子菜单自动消失
Dec 29 Javascript
JS实现根据出生年月计算年龄
Jan 10 Javascript
深入理解javascript的执行顺序
Apr 04 Javascript
JavaScript电子时钟倒计时第二款
Jan 10 Javascript
JavaScript实现的SHA-1加密算法完整实例
Feb 02 Javascript
JavaScript知识点总结(十)之this关键字
May 31 Javascript
Vuejs第十三篇之组件——杂项
Sep 09 Javascript
JS无缝滚动效果实现方法分析
Dec 21 Javascript
浅谈Vue.js中的v-on(事件处理)
Sep 05 Javascript
详解如何使用 vue-cli 开发多页应用
Dec 16 Javascript
Vue框架之goods组件开发详解
Jan 25 Javascript
微信小程序实现星级评价
Nov 20 #Javascript
微信小程序音乐播放器开发
Nov 20 #Javascript
微信小程序实现音乐播放器
Nov 20 #Javascript
vue移动端模态框(可传参)的实现
Nov 20 #Javascript
微信小程序实现上拉加载功能
Nov 20 #Javascript
微信小程序实现锚点功能
Nov 20 #Javascript
vue实现element表格里表头信息提示功能(推荐)
Nov 20 #Javascript
You might like
PHP的autoload机制的实现解析
2012/09/15 PHP
php中解析带中文字符的url函数分享
2015/01/20 PHP
php遍历删除整个目录及文件的方法
2015/03/13 PHP
在Mac OS的PHP环境下安装配置MemCache的全过程解析
2016/02/15 PHP
thinkphp关于简单的权限判定方法
2017/04/03 PHP
Jquery 表单取值赋值的一些基本操作
2009/10/11 Javascript
JS实现随机数生成算法示例代码
2013/08/08 Javascript
JavaScript包装对象使用介绍
2013/08/29 Javascript
arguments对象验证函数的参数是否合法
2015/06/26 Javascript
jQuery实现网页顶部固定导航效果代码
2015/12/24 Javascript
js面向对象的写法
2016/02/19 Javascript
用vue和node写的简易购物车实现
2017/04/25 Javascript
Angular2.0/4.0 使用Echarts图表的示例代码
2017/12/07 Javascript
解决vue 格式化银行卡(信用卡)每4位一个符号隔断的问题
2018/09/14 Javascript
JQuery判断radio单选框是否选中并获取值的方法
2019/01/17 jQuery
服务端预渲染之Nuxt(使用篇)
2019/04/08 Javascript
echarts大屏字体自适应的方法步骤
2019/07/12 Javascript
JavaScript canvas实现雪花随机动态飘落
2020/02/08 Javascript
Element InputNumber 计数器的实现示例
2020/08/03 Javascript
Python中使用HTMLParser解析html实例
2015/02/08 Python
Django admin model 汉化显示文字的实现方法
2019/08/12 Python
Python将视频或者动态图gif逐帧保存为图片的方法
2019/09/10 Python
python__name__原理及用法详解
2019/11/02 Python
Python3使用腾讯云文字识别(腾讯OCR)提取图片中的文字内容实例详解
2020/02/18 Python
python GUI库图形界面开发之PyQt5单选按钮控件QRadioButton详细使用方法与实例
2020/02/28 Python
python连接mongodb数据库操作数据示例
2020/11/30 Python
python中time、datetime模块的使用
2020/12/14 Python
水芝澳美国官网:H2O Plus
2016/10/15 全球购物
国际旅客访问北美最大的汽车租赁提供商:Alamo Rent A Car
2018/06/13 全球购物
班主任工作年限证明
2014/01/12 职场文书
技校毕业生自荐信范文
2014/03/07 职场文书
创业者迈进成功第一步:如何写创业计划书?
2014/03/22 职场文书
我的梦想演讲稿1000字
2014/08/21 职场文书
优秀三好学生事迹材料
2014/08/31 职场文书
导游词之新疆-喀纳斯
2019/10/10 职场文书
Python实现简单的猜单词
2021/06/15 Python