在年初开发一个中后台管理系统,功能涉及到了各个部门(产品、客服、市场等等),在开始的版本中,我和后端配合使用了花裤衩手摸手系列的权限方案,前期非常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 值,不然当所有子路由都没有权限时,父级路由就被认为是无权限控制且可访问的;而如果子路由中只要有一个路由无权限控制,那就不需要处理父路由。所以这里可以用递归来解决,先处理子路由再处理父路由