如何优雅的在 vue 中添加权限控制

前言

在一个项目中,一些功能会涉及到重要的数据管理,为了确保数据的安全,咱们会在项目中加入权限来限制每一个用户的操做。做为前端,咱们要作的是配合后端给到的权限数据,作页面上的各类各样的限制。html

需求

由于这是一个工做上的业务需求,因此对于我来讲主要有两个地方须要进行权限控制。前端

第一个是侧边菜单栏,须要控制显示与隐藏。vue

第二个就是页面内的各个按钮,弹窗等。vue-router

流程

  1. 如何获取用户权限?vuex

    后端(当前用户拥有的权限列表)-> 前端(经过后端的接口获取到,下文中咱们把当前用户的权限列表叫作 permissionList)后端

  2. 前端如何作限制?api

    经过产品的需求,在项目中进行权限点的配置,而后经过 permissionList 寻找是否有配置的权限点,有就显示,没有就不显示。数组

  3. 而后呢?缓存

    没了。安全

当我刚开始接到这个需求的时候就是这么想的,这有什么难的,不就获取 permissionList 而后判断就能够了嘛。后来我才发现真正的需求远比我想象的复杂。

真正的问题

上面的需求有提到咱们主要解决两个问题,侧边菜单栏的显示 & 页面内操做。

假设咱们有这样一个路由的设置(如下只是一个例子):

import VueRouter from 'vue-router'
/* 注意:如下配置仅为部分配置,而且省去了 component 的配置 */
export const routes = [
  {
    path: '/',
    name: 'Admin',
    label: '首页'
  },
  {
    path: '/user',
    name: 'User',
    label: '用户',
    redirect: { name: 'UserList' },
    children: [
      {
        path: 'list',
        name: 'UserList',
        label: '用户列表'
      },
      {
        path: 'group',
        name: 'UserGroup',
        label: '用户组',
        redirect: { name: 'UserGroupList' },
        children: [
          {
            path: 'list',
            name: 'UserGroupList',
            label: '用户组列表'
          },
          {
            path: 'config',
            name: 'UserGroupConfig',
            label: '用户组设置'
          }
        ]
      }
    ]
  },
  {
    path: '/setting',
    name: 'Setting',
    label: '系统设置'
  },
  {
    path: '/login',
    name: 'Login',
    label: '登陆'
  }
]

const router = new VueRouter({
  routes
})

export default router
复制代码

其中前两级路由会显示在侧边栏中,第三级就不会显示在侧边栏中了。

页面内操做的权限设置不须要考虑不少其余东西,咱们主要针对侧边栏以及路由进行问题的分析,经过分析,主要有如下几个问题:

  1. 何时获取 permissionList,如何存储 permissionList
  2. 子路由全都没权限时不该该显示自己(例:当用户列表和用户组都没有权限时,用户也不该该显示在侧边栏)
  3. 默认重定向的路由没有权限时,应寻找 children 中有权限的一项重定向(例:用户路由重定向到用户列表路由,若用户列表没有权限,则应该重定向到用户组路由)
  4. 当用户直接输入没有权限的 url 时须要跳转到没有权限的页面或其余操做。(路由限制)

下面咱们针对以上问题一个一个解决。

何时获取权限,存储在哪 & 路由限制

我这里是在 routerbeforeEach 中获取的,获取的 permissionList 是存放在 vuex 中。

缘由是考虑到要作路由的限制,以及方便后面项目中对权限列表的使用,如下是实现的示例:

首先咱们加入权限配置到 router 上:

// 如下只展现部分配置
{
  path: '/user',
  name: 'User',
  label: '用户',
  meta: {
    permissions: ['U_1']
  },
  redirect: { name: 'UserList' },
  children: [
    {
      path: 'list',
      name: 'UserList',
      label: '用户列表',
      meta: {
        permissions: ['U_1_1']
      }
    },
    {
      path: 'group',
      name: 'UserGroup',
      label: '用户组',
      meta: {
        permissions: ['U_1_2']
      },
      redirect: { name: 'UserGroupList' },
      children: [
        {
          path: 'list',
          name: 'UserGroupList',
          label: '用户组列表',
          meta: {
            permissions: ['U_1_2_1']
          }
        },
        {
          path: 'config',
          name: 'UserGroupConfig',
          label: '用户组设置',
          meta: {
            permissions: ['U_1_2_2']
          }
        }
      ]
    }
  ]
}
复制代码

能够看到咱们把权限加在了 meta 上,是为了更简单的从 router.beforeEch 中进行权限判断,权限设置为一个数组,是由于一个页面可能涉及多个权限。

接下来咱们设置 router.beforeEach :

// 引入项目的 vuex
import store from '@/store'
// 引入判断是否拥有权限的函数
import { includePermission } from '@/utils/permission'

router.beforeEach(async (to, from, next) => {
  // 先判断是否为登陆,登陆了才能获取到权限,怎么判断登陆就不写了
  if (!isLogin) {
    try {
      // 这里获取 permissionList
      await store.dispatch('getPermissionList')
      // 这里判断当前页面是否有权限
      const { permissions } = to.meta
      if (permissions) {
        const hasPermission = includePermission(permissions)
        if (!hasPermission) next({ name: 'NoPermission' })
      }
      next()
    }
  } else {
    next({ name: 'Login' })
  }
})
复制代码

咱们能够看到咱们须要一个判断权限的方法 & vuex 中的 getPermissionList 以下:

// @/store
export default {
  state: {
    permissionList: []
  },
  mutations: {
    updatePermissionList: (state, payload) => {
      state.permissionList = payload
    }
  },
  actions: {
    getPermissionList: async ({ state, commit }) => {
      // 这里是为了防止重复获取
      if (state.permissionList.length) return
      // 发送请求方法省略
      const list = await api.getPermissionList()
      commit('updatePermissionList', list)
    }
  }
}
复制代码
// @/utils/permission
import store from '@/store'

/** * 判断是否拥有权限 * @param {Array<string>} permissions - 要判断的权限列表 */
function includePermission (permissions = []) {
  // 这里要判断的权限没有设置的话,就等于不须要权限,直接返回 true
  if (!permissions.length) return true
  const permissionList = store.state.permissionList
  return !!permissions.find(permission => permissionList.includes(permission))
}
复制代码

重定向问题

以上咱们解决了路由的基本配置与权限如何获取,怎么限制路由跳转,接下来咱们要处理的就是重定向问题了。

这一点可能和咱们项目自己架构有关,咱们项目的侧边栏下还有子级,是如下图中的 tab 切换展示的,正常状况当点击药品管理后页面会重定向到入库管理的 tab 切换页面,但当入库管理没有权限时,则应该直接重定向到出库管理界面。

因此想实现以上的效果,我须要重写 router 的 redirect,作到能够动态判断(由于在我配置路由时并不知道当前用户的权限列表)

而后我查看了 vue-router 的文档,发现了 redirect 能够是一个方法,这样就能够解决重定向问题了。

vue-router 中 redirect 说明 ,根听说明咱们能够改写 redirect 以下:

// 咱们须要引入判断权限方法
import { includePermission } from '@/utils/permission'

const children = [
  {
    path: 'list',
    name: 'UserList',
    label: '用户列表',
    meta: {
      permissions: ['U_1_1']
    }
  },
  {
    path: 'group',
    name: 'UserGroup',
    label: '用户组',
    meta: {
      permissions: ['U_1_2']
    }
  }
]

const routeDemo = {
  path: '/user',
  name: 'User',
  label: '用户',
  redirect: (to) => {
    if (includePermission(children[0].meta.permissions)) return { name: children[0].name }
    if (includePermission(children[1].meta.permissions)) return { name: children[1].name }
  },
  children
}
复制代码

虽然问题解决了,可是发现这样写下去很麻烦,还要修改 router 的配置,因此咱们使用一个方法生成:

// @/utils/permission
/** * 建立重定向函数 * @param {Object} redirect - 重定向对象 * @param {string} redirect.name - 重定向的组件名称 * @param {Array<any>} children - 子列表 */
function createRedirectFn (redirect = {}, children = []) {
  // 避免缓存太大,只保留 children 的 name 和 permissions
  const permissionChildren = children.map(({ name = '', meta: { permissions = [] } = {} }) => ({ name, permissions }))
  return function (to) {
    // 这里必定不能在 return 的函数外面筛选,由于权限是异步获取的
    const hasPermissionChildren = permissionChildren.filter(item => includePermission(item.permissions))
    // 默认填写的重定向的 name
    const defaultName = redirect.name || ''
    // 若是默认重定向没有权限,则从 children 中选择第一个有权限的路由作重定向
    const firstPermissionName = (hasPermissionChildren[0] || { name: '' }).name
    // 判断是否须要修改默认的重定向
    const saveDefaultName = !!hasPermissionChildren.find(item => item.name === defaultName && defaultName)
    if (saveDefaultName) return { name: defaultName }
    else return firstPermissionName ? { name: firstPermissionName } : redirect
  }
}
复制代码

而后咱们就能够改写为:

// 咱们须要引入判断权限方法
import { includePermission, createRedirectFn } from '@/utils/permission'

const children = [
  {
    path: 'list',
    name: 'UserList',
    label: '用户列表',
    meta: {
      permissions: ['U_1_1']
    }
  },
  {
    path: 'group',
    name: 'UserGroup',
    label: '用户组',
    meta: {
      permissions: ['U_1_2']
    }
  }
]

const routeDemo = {
  path: '/user',
  name: 'User',
  label: '用户',
  redirect: createRedirectFn({ name: 'UserList' }, children),
  children
}
复制代码

这样稍微简洁一些,但我仍是须要一个一个路由去修改,因此我又写了一个方法来递归 router 配置,并重写他们的 redirect:

// @/utils/permission
/** * 建立有权限的路由配置(多级) * @param {Object} config - 路由配置对象 * @param {Object} config.redirect - 必须是 children 中的一个,而且使用 name */
function createPermissionRouter ({ redirect, children = [], ...others }) {
  const needRecursion = !!children.length
  if (needRecursion) {
    return {
      ...others,
      redirect: createRedirectFn(redirect, children),
      children: children.map(item => createPermissionRouter(item))
    }
  } else {
    return {
      ...others,
      redirect
    }
  }
}
复制代码

这样咱们只须要在最外层的 router 配置加上这样一层函数就能够了:

import { createPermissionRouter } from '@/utils/permission'

const routesConfig = [
  {
    path: '/user',
    name: 'User',
    label: '用户',
    meta: {
      permissions: ['U_1']
    },
    redirect: { name: 'UserList' },
    children: [
      {
        path: 'list',
        name: 'UserList',
        label: '用户列表',
        meta: {
          permissions: ['U_1_1']
        }
      },
      {
        path: 'group',
        name: 'UserGroup',
        label: '用户组',
        meta: {
          permissions: ['U_1_2']
        },
        redirect: { name: 'UserGroupList' },
        children: [
          {
            path: 'list',
            name: 'UserGroupList',
            label: '用户组列表',
            meta: {
              permissions: ['U_1_2_1']
            }
          },
          {
            path: 'config',
            name: 'UserGroupConfig',
            label: '用户组设置',
            meta: {
              permissions: ['U_1_2_2']
            }
          }
        ]
      }
    ]
  }
]

export const routes = routesConfig.map(item => createPermissionRouter(item))

const router = new VueRouter({
  routes
})

export default router
复制代码

固然这样写还有一个好处,其实你并不须要设置 redirect,这样会自动重定向到 children 的第一个有权限的路由

侧边栏显示问题

咱们的项目使用的是根据路由的配置来生成侧边栏的,固然会加一些其余的参数来显示显示层级等问题,这里就不写具体代码了,如何解决侧边栏 children 全都无权限不显示的问题呢。

这里个人思路是,把路由的配置也一同更新到 vuex 中,而后侧边栏配置从 vuex 中的配置来读取。

因为这个地方涉及修改的东西有点多,并且涉及业务,我就不把代码拿出来了,你能够自行实验。

方便团队部署权限点的方法

以上咱们解决了大部分权限的问题,那么还有不少涉及到业务逻辑的权限点的部署,因此为了团队中其余人能够优雅简单的部署权限点到各个页面中,我在项目中提供了如下几种方式来部署权限:

  1. 经过指令 v-permission 来直接在 template 上设置
<div v-permission="['U_1']"></div>
复制代码
  1. 经过全局方法 this.$permission 判断,由于有些权限并不是在模版中的
{
  hasPermission () {
    // 经过方法 $permission 判断是否拥有权限
    return this.$permission(['U_1_1', 'U_1_2'])
  }
}
复制代码

这里要注意,为了 $permission 方法的返回值是可被监测的,判断时须要从 this.$store 中来判断,如下为实现代码:

// @/utils/permission
/** * 判断是否拥有权限 * @param {Array<string|number>} permissions - 要判断的权限列表 * @param {Object} permissionList - 传入 store 中的权限列表以实现数据可监测 */
function includePermissionWithStore (permissions = [], permissionList = []) {
  if (!permissions.length) return true
  return !!permissions.find(permission => permissionList.includes(permission))
}
复制代码
import { includePermissionWithStore } from '@/utils/permission'
export default {
  install (Vue, options) {
    Vue.prototype.$permission = function (permissions) {
      const permissionList = this.$store.state.permissionList
      return includePermissionWithStore(permissions, permissionList)
    }
  }
}
复制代码

如下为指令的实现代码(为了避免与 v-if 冲突,这里控制显示隐藏经过添加/移除 className 的方式):

// @/directive/permission
import { includePermission } from '@/utils/permission'
const permissionHandle = (el, binding) => {
  const permissions = binding.value
  if (!includePermission(permissions)) {
    el.classList.add('hide')
  } else {
    el.classList.remove('hide')
  }
}
export default {
  inserted: permissionHandle,
  update: permissionHandle
}
复制代码

总结

针对以前的问题,有如下的总结:

  1. 何时获取 permissionList,如何存储 permissionList

    router.beforeEach 获取,存储在 vuex。

  2. 子路由全都没权限时不该该显示自己(例:当用户列表和用户设置都没有权限时,用户也不该该显示在侧边栏)

    经过存储路由配置到 vuex 中,生成侧边栏设置,获取权限后修改 vuex 中的配置控制显示 & 隐藏。

  3. 默认重定向的路由没有权限时,应寻找 children 中有权限的一项重定向(例:用户路由重定向到用户列表路由,若用户列表没有权限,则应该重定向到用户组路由)

    经过 vue-routerredirect 设置为 Function 来实现

  4. 当用户直接输入没有权限的 url 时须要跳转到没有权限的页面或其余操做。(路由限制)

    在 meta 中设置权限, router.beforeEach 中判断权限。

以上就是我对于此次权限需求的大致解决思路与代码实现,可能并非很完美,但仍是但愿能够帮助到你 ^_^

相关文章
相关标签/搜索