权限受控解决方案之分级分控权限管理

309 阅读5分钟

权限理论:RBAC 权限控制体系

看下一个RBAC权限控制体系,需要那几个页面:

  1. 员工原理 这个页面是供管理员配置员工信息,包括设置员工角色,excel批量导入员工及导出

image.png

  1. 角色列表 该页面配置了那些角色,每个角色对应不同的权限

image.png

image.png

  1. 权限列表 这个页面展示有哪些权限,这些权限对应到角色列表中

image.png

上面三个页面就形成了:用户 -> 角色 -> 权限 的一个分配关系。

当我们通过角色为某一个用户指定到不同的权限之后,那么该用户就会在 项目中体会到不同权限的功能。那么这样的一套关系就是我们的 RBAC 权限控制体系,也就是 基于 角色的权限 控制 用户的访问

页面权限vs功能权限

  1. 页面权限: 当前用户可以访问的页面
  2. 功能权限: 当前用户可以访问的权限功能(并非所有功能有需要权限)

页面权限

所谓页面权限包含两部分内容:

  1. 用户可看到的:左侧 menu 菜单的 item 展示
  2. 用户看不到的:路由表配置

可以来看一下 路由表配置了,把路由表分成了两部分:

  1. 私有路由表 privateRoutes:依据权限进行动态配置的
  2. 公开路由表 publicRoutes:无权限要求的

我们期望的是:不同的权限进入系统可以看到不同的路由 。那么换句话而言就是:根据不同的权限数据,生成不同的私有路由表

  1. 页面权限实现的核心在于 路由表配置
  2. 路由表配置的核心在于 私有路由表 privateRoutes
  3. 私有路由表 privateRoutes 的核心在于 addRoute API

实现步骤:

  1. 页面权限数据在 userInfo -> permission -> menus 之中 image.png

  2. 私有路由表不再被直接加入到 routes 中

export const privateRoutes = [...]
export const publicRoutes = [...]

const router = createRouter({
  history: createWebHashHistory(),
  routes: publicRoutes
})
  1. 利用 addRoute API 动态添加路由到 路由表 中

(1). 创建 store/modules/permission 模块:

// 专门处理权限路由的模块
import { publicRoutes, privateRoutes } from '@/router'
export default {
  namespaced: true,
  state: {
    // 路由表:初始拥有静态路由权限
    routes: publicRoutes
  },
  mutations: {
    // 增加路由
    setRoutes(state, newRoutes) {
      // 永远在静态路由的基础上增加新路由
      state.routes = [...publicRoutes, ...newRoutes]
    }
  },
  actions: {
     // 根据权限筛选路由
    filterRoutes(context, menus) {}
  }
}

(2). 那么 filterRoutes 这个动作我们怎么制作呢?为每个权限路由指定一个 name,每个 name 对应一个页面权限。

export default {
  path: '/user',
  component: layout,
  redirect: '/user/manage',
  // 给一个name,用来filter
  name: 'roleList',
  meta: {
    title: 'user',
    icon: 'personnel'
  },
  children: []
}

(3). 把router进行分模块管理,此时所有的权限页面都拥有一个名字,这个名字与权限数据匹配

import ArticleCreaterRouter from './modules/ArticleCreate'
import ArticleRouter from './modules/Article'
import PermissionListRouter from './modules/PermissionList'
import RoleListRouter from './modules/RoleList'
import UserManageRouter from './modules/UserManage'

export const asyncRoutes = [
  RoleListRouter,
  UserManageRouter,
  PermissionListRouter,
  ArticleCreaterRouter,
  ArticleRouter
]

(4). 生成权限路由表数据

// 根据权限筛选路由
filterRoutes(context, menus) {
  const routes = []
  // 路由权限匹配
  menus.forEach(key => {
    // 权限名 与 路由的 name 匹配
    routes.push(...privateRoutes.filter(item => item.name === key))
  })
  // 最后添加 不匹配路由进入 404
  routes.push({
    path: '/:catchAll(.*)',
    redirect: '/404'
  })
  context.commit('setRoutes', routes)
  return routes
}

(5). 在 src/permission 中,获取用户数据之后调用该动作

// 判断用户资料是否获取
  // 若不存在用户信息,则需要获取用户信息
  if (!store.getters.hasUserInfo) {
    // 触发获取用户信息的 action,并获取用户当前权限
    const { permission } = await store.dispatch('user/getUserInfo')
    // 处理用户权限,筛选出需要添加的权限
    const filterRoutes = await store.dispatch(
      'permission/filterRoutes',
      permission.menus
    )
    // 利用 addRoute 循环添加
    filterRoutes.forEach(item => {
      router.addRoute(item)
    })
    // 添加完动态路由之后,需要在进行一次主动跳转
    return next(to.path)
  }
  next()

(6). 重置路由表数据

现在有个问题:重新登录权限账户,不刷新页面,左侧菜单不会自动改变。出现这个问题的原因其实非常简单:退出登录时,添加的路由表并未被删除。那么删除动态添加的路由可以使用 removeRoute 方法进行。

在 router/index 中定义 resetRouter 方法

export function resetRouter() {
  if (
    store.getters.userInfo &&
    store.getters.userInfo.permission &&
    store.getters.userInfo.permission.menus
  ) {
    const menus = store.getters.userInfo.permission.menus
    menus.forEach((menu) => {
      // removeRoute 通过名称name删除现有路由
      router.removeRoute(menu)
    })
  }

在退出登录的动作下,触发该方法:

logout(context) {
   resetRouter()
}

功能权限

对于功能权限而言,我们只需要:根据权限数据,隐藏功能按钮 即可。隐藏的方式我们可以通过指令进行。vue3指令编写参考vue3 自定义指令

  1. 我们期望最终可以通过这样格式的指令进行功能受控 v-permission="['importUser']"
  2. 以此创建对应的自定义指令 directives/permission
import store from '@/store'

function checkPermission(el, binding) {
  // 获取绑定的值,此处为权限
  const { value } = binding
  // 获取所有的功能指令
  const points = store.getters.userInfo.permission.points
  // 当传入的指令集为数组时
  if (value && value instanceof Array) {
    // 匹配对应的指令
    const hasPermission = points.some((point) => {
      return value.includes(point)
    })
    // 如果无法匹配,则表示当前用户无该指令,那么删除对应的功能按钮
    if (!hasPermission) {
      el.parentNode && el.parentNode.removeChild(el)
    }
  } else {
    // eslint-disabled-next-line
    throw new Error('v-permission value is ["admin","editor"]')
  }
}

export default {
  // 在绑定元素的父组件被挂载后调用
  mounted(el, binding) {
    checkPermission(el, binding)
  },
  // 在包含组件的 VNode 及其子组件的 VNode 更新后调用
  update(el, binding) {
    checkPermission(el, binding)
  }
}

  1. 在 directives/index 中绑定该指令
import permission from './permission'
export default (app) => {
  ...
  app.directive('permission', permission)
}
  1. 在所有功能中,添加该指令
<el-button v-permission="['distributePermission']">
{{ $t('msg.role.assignPermissions') }}
</el-button>