一个简单的Vue按钮级权限方案

演示前端

场景

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

  1. 手摸手系列权限方案是有比较清晰的权限划分的,而咱们公司部门的岗位职责有时比较模糊。
  2. 后端采用RBAC权限方案,为了达到第1点要求,将角色划分的很细,而且角色有时频繁变更,致使每一次前端都须要手动维护

为了解决上面2个痛点,我将原方案进行了一丢丢改造。ios

  1. 前端再也不以角色来控制权限,而是以更小粒度的操做(接口)来控制,也就是前端不关心角色
  2. 路由仍是由前端维护(咱们的后端很排斥维护和他们不相干的东西😂),但改成经过操做列表对权限路由进行过滤
  3. 使用单一的方式(方便维护)控制页面的局部权限,再也不使用自定义指令方式,而是经过函数式组件,缘由是使用自定义指令有多余的开销(插入再移除)

后端的配合:git

  1. 提供一个获取当前用户操做列表的接口
  2. 操做列表须要增长一个惟一标识(操做码)供前端使用,不变的
  3. 操做列表须要增长一个routerName字段,用于可视化权限编辑

有一些注意点:github

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

实现

操做列表示例

以Restful风格接口为例ajax

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种则视为无权限控制,任何用户都能访问axios

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

  1. hidden,值为true时,路由包括子路由都不会出如今菜单中
  2. visible,值为false时,路由不显示,但显示子路由
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撸后台 系列二(登陆权限篇)

相关文章
相关标签/搜索