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之一
Apr 27 Javascript
jquery实现瀑布流效果分享
Mar 26 Javascript
jQuery使用height()获取高度需要注意的地方
Dec 13 Javascript
jquery使用slideDown实现模块缓慢拉出效果的方法
Mar 27 Javascript
JS实现淡蓝色简洁竖向Tab点击切换效果
Oct 06 Javascript
JavaScript获取IP获取的是IPV6 如何校验
Jun 12 Javascript
基于JQuery实现分隔条的功能
Jun 17 Javascript
ES6中的数组扩展方法
Aug 26 Javascript
JavaScript之面向对象_动力节点Java学院整理
Jun 29 Javascript
jquery实现点击a链接,跳转之后,该a链接处显示背景色的方法
Jan 18 jQuery
JS实现换肤功能的方法实例详解
Jan 30 Javascript
微信小程序在text文本实现多种字体样式
Nov 08 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
晋城吧对DiscuzX进行的前端优化要点
2010/09/05 PHP
php基于表单密码验证与HTTP验证用法实例
2015/01/06 PHP
PDO防注入原理分析以及注意事项
2015/02/25 PHP
一个简单至极的PHP缓存类代码
2015/10/23 PHP
php+ajax注册实时验证功能
2016/07/20 PHP
PHP执行系统命令函数实例讲解
2021/03/03 PHP
ASP.NET jQuery 实例5 (显示CheckBoxList成员选中的内容)
2012/01/13 Javascript
深入理解JavaScript系列(11) 执行上下文(Execution Contexts)
2012/01/15 Javascript
JS简单实现登陆验证附效果图
2013/11/19 Javascript
JavaScript中的值类型详细介绍
2014/12/29 Javascript
谈谈我对JavaScript原型和闭包系列理解(随手笔记6)
2015/12/20 Javascript
js手机号批量滚动抽奖实现代码
2020/04/17 Javascript
vue+vuex+axio从后台获取数据存入vuex实现组件之间共享数据
2017/04/22 Javascript
Node对CommonJS的模块规范
2019/11/06 Javascript
jquery ajax 请求小技巧实例分析
2019/11/11 jQuery
jQuery HTML设置内容和属性操作实例分析
2020/05/20 jQuery
python查询mysql中文乱码问题
2014/11/09 Python
python实现自动更换ip的方法
2015/05/05 Python
python使用Pycharm创建一个Django项目
2018/03/05 Python
对Python3 pyc 文件的使用详解
2019/02/16 Python
Python 200行代码实现一个滑动验证码过程详解
2019/07/11 Python
django ManyToManyField多对多关系的实例详解
2019/08/09 Python
Python3简单爬虫抓取网页图片代码实例
2019/08/26 Python
美国在线家装零售商:Build.com
2016/09/02 全球购物
StubHub哥伦比亚:购买和出售您的门票
2016/10/20 全球购物
Foot Locker澳洲官网:美国运动服和鞋类零售商
2019/10/11 全球购物
大学自我评价
2014/02/12 职场文书
大一学生职业生涯规划
2014/03/11 职场文书
股份转让协议书
2014/04/12 职场文书
电子专业毕业生自荐信
2014/05/25 职场文书
幼儿园中班区域活动总结
2014/07/09 职场文书
副总经理党的群众路线教育实践活动个人对照检查材料思想汇报
2014/10/06 职场文书
车辆年检委托书范本
2014/10/14 职场文书
2014年高中教师工作总结
2014/12/19 职场文书
Mongo服务重启异常问题的处理方法
2021/07/01 MongoDB
Python+OpenCV实现在图像上绘制矩形
2022/03/21 Python