美文网首页高德地图react & vue & angular
Vue3写一个后台管理系统(4)RBAC权限受控体系的实现

Vue3写一个后台管理系统(4)RBAC权限受控体系的实现

作者: 程序员三千_ | 来源:发表于2023-03-13 10:53 被阅读0次

    一、RBAC 权限控制体系

    要实现动态Menu,我们需要先来统一一下认知,明确项目中的权限控制系统。
    网上找了张图,我们可以大致的看下


    image.png

    从图中,我们可以简单的这样理解RBAC 权限控制体系。

    • 用户:我们登录后台管理系统的账号。举个例子:张三这个人,我们可以认为他是一个用户
    • 角色:用户的“头衔”。张三是一个销售经理,那么“销售经理”,我们可以认为他是一个角色。
    • 权限:每个角色都有不同的权限。“销售经理”这个角色,可以查看、删除、编辑客户资料,那么张三就可以查看、删除、编辑客户资料,这时候如果有个李四,李四是普通的“销售”的角色,而普通的“销售”只能查看客户信息,不能删除、编辑客户信息,所以李四只能查看客户信息。

    那么明确好了 RBAC 的概念之后,接下来我们就可以来去实现我们的辅助业务了,所谓辅助业务具体指的就是:

    • 员工管理(用户列表)
      1. 为用户分配角色
    • 角色列表
      1. 角色列表展示
      2. 为角色分配权限
    • 权限列表
      1. 权限列表展示

    我们先直接做好的后台先看看效果,明确下RBAC在我们后台管理系统中的含义。


    image.png
    image.png

    我们从上面两张图中,可以看到,账号(test),是一个“测试-角色”的角色,
    而测试角色的只能看到下面的菜单(权限列表)


    image.png

    而如果我们用超管的账号登录进去,是能看到所有的菜单(权限列表)的


    image.png

    那么由此呈现我们可以看出,整个权限系统其实分成了两部分:

    1. 页面权限:根据不同的 权限数据,展示不同的页面(就是展示不同的菜单Menu,因为一个菜单按钮,是对应一个具体的页面)
    2. 功能权限:根据不同的 权限数据,一个页面里展示不同的 功能按钮

    二、下面我们说下代码实现的逻辑

    1. 页面权限实现的核心在于 路由表配置

    2. 路由表配置的核心在于 私有路由表 privateRoutes

    3. 私有路由表 privateRoutes 的核心在于 addRoute API

    那么简单一句话总结,我们只需要:根据不同的权限数据,利用 addRoute API 生成不同的私有路由表 即可实现 页面权限 功能

    而*实现功能权限的核心在于 根据数据隐藏功能按钮,那么隐藏的方式我们可以通过Vue的指令进行控制

    三、页面权限代码实现

    首先我们的路由表需要分成公有路由表私有路由表

    • 私有路由表:就是不同角色拥有不同的路由表
    • 共有路由表:就是每个角色都有的路由表:例如登录界面、404界面、401界面
      讲清了这些下面实现起来也是很简单的,只是一些细节可能要注意,那么直接看代码吧,代码里都有注释
    • 创建每一个私有路由表


      image.png

    其中一个路由表的代码,其他都是类似的,要注意的是每个路由表的path是要不和服务端返回的path相同的,我们到时候是根据路由的path去筛选数据的,这里我用到的所有界面都是test-page页面,但不影响具体大逻辑,大家明白就行

    const RightRouter = {
      path: '/manage',
      component: Layout,
      redirect: '/manage/manageList',
      alwaysShow: true, // will always show the root menu
      name: 'manage',
      meta: {
        title: '管理1',
        icon: 'el-icon-s-check'
      },
      children: [
        {
          path: '/manage/manageList',
          component: () => import('@/views/test-page/index.vue'),
          name: 'list1',
          meta: { title: '列表1' }
        },
        {
          path: '/manage/manageList2',
          component: () => import('@/views/test-page/index.vue'),
          name: 'rightSetList',
          meta: { title: '列表2' }
        }
    
      ]
    }
    
    export default RightRouter
    
    
    • 把每个路由表合并到privateRoutes
    /**
     * 私有路由表
     */
    export var privateRoutes = [
      permissions,
      manageList,
    
    ]
    /**
     * 公开路由表
     */
    export var publicRoutes = [
      {
        path: '/login',
        component: () => import('@/views/login/index')
      },
      {
        path: '/',
        // 注意:带有路径“/”的记录中的组件“默认”是一个不返回 Promise 的函数
        component: layout,
        redirect: '/home',
        children: [
          {
            path: '/home',
            name: 'home',
            component: () => import('@/views/home/index'),
            meta: {title: '首页', affix: true},//affix=true,tagViews右侧没有关闭按钮
            hidden: true,//不显示在侧边栏
          },
          {
            path: '/404',
            name: '404',
            component: () => import('@/views/error-page/404')
          },
          {
            path: '/401',
            name: '401',
            component: () => import('@/views/error-page/401')
          }
        ]
      }
    ]
    
    const router = createRouter({
      history: createWebHashHistory(),
      // routes: [...publicRoutes, ...privateRoutes]
      routes: publicRoutes
    
    })
    
    export default router
    
    

    我们先看下接口返回的数据


    image.png
    image.png

    从接口返回的数据中我们能可以看出,一级菜单和二级菜单都是有一个url字段的,我们就是要根据这个url字段和我门路由表的path字段去做对表,如果存在,就渲染这个路由,不存在就不去渲染这个路由,所以我们需要先将服务端返回的路由数据,转化成这个格式的数据


    image.png

    筛选路由的具体方法代码

    
    
    /**
     * 根据服务端返回的路由数据,筛选过滤本地的路由数据
     * @param routes asyncRoutes 本地写的数据
     * @param roles 接口获取的数据
     */
    export function filterPrivateRoutes(routes, roles) {
      const res = []
      routes.forEach(route => {
        const tmp = { ...route }
        //检查是否符合权限规则:根据自己公司定义的规则
        if (hasPermission(roles, tmp)) {
          if (tmp.children) {
            tmp.children = filterPrivateRoutes(tmp.children, roles)
          }
          res.push(tmp)
        }
      })
    
      return res
    }
    
    export default {
      namespaced: true,
      state: {
        // 路由表:初始拥有静态路由权限
        routes: publicRoutes
      },
      mutations: {
        /**
         * 增加路由
         */
        setRoutes(state, newRoutes) {
          // 永远在静态路由的基础上增加新路由
          state.routes = [...publicRoutes, ...newRoutes]
        }
      },
      actions: {
    
    }
    
    
    

    最后,在在 src/permission 中,获取路由数据之后调用这些代码,相关注释都写到代码里了

    
    // 白名单
    const whiteList = ['/login']
    /**
     * 路由前置守卫
     */
    router.beforeEach(async (to, from, next) => {
           ....................
            const {roles} = await store.dispatch('user/getPermissionData')
            // 处理用户权限,筛选出需要添加的权限
            const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
    
            console.log("筛选出需要addRoute的路由",accessRoutes)
            // 利用 addRoute 循环添加
            accessRoutes.forEach(item => {
              router.addRoute(item)
            })
            // router.addRoutes(accessRoutes)
              // hack method to ensure that addRoutes is complete
              // set the replace: true, so the navigation will not leave a history record
            next({...to, replace: true})
        ........................
    
    
    

    到这里动态菜单差不多就讲完了,但还有一个问题,就是如果我们更换和账户的登录,只有手动刷新下页面,左边菜单才会改变,不会自动去改变。这是因为我们退出的时候,没有重置路由表。所以我们在退出的时候,重置下就行了

    
    /**
     * 重置路由表
     */
    export function resetRouter() {
      if (store.getters.hasRoles) {
        const menus = store.getters.roles
        //removeRoute是根据路由的name去删除路由的,所以我们要对路由的名字进行截取
        // const menus = ['getRoleList','admintorList','adminAuth']
        // console.log("menus==",menus)
        // console.log("router==",router.getRoutes())
        menus.forEach(menu => {
          let url = menu.url
          let i = url.lastIndexOf('/')
          let name = url.substring(i+1,url.length)
          router.removeRoute(name)
        })
      }
    
    }
    
    
    import router, { resetRouter } from '@/router'
    
    logout(context) {
          resetRouter()
          ...
        }
    
    

    四、功能权限代码实现

    所以首先我们先去创建这样一个指令(vue3 自定义指令

    1. 我们期望最终可以通过这样格式的指令进行功能受控 v-permission="'/adminAuth/admintorList'"

    2. 以此创建对应的自定义指令 directives/permission

    import store from '@/store'
    import {lowerCase} from '@/utils/index'
    
    function checkPermission(el, binding) {
      // 获取绑定的值,此处为权限
      const value = lowerCase(binding.value);
      const auths = store.getters.buttons || [];
      if (!auths.includes(value)) {
        el.parentNode.removeChild(el);
      }
    }
    
    export default {
      // 在绑定元素的父组件被挂载后调用
      mounted(el, binding) {
        checkPermission(el, binding)
      },
      // 在包含组件的 VNode 及其子组件的 VNode 更新后调用
      update(el, binding) {
        checkPermission(el, binding)
      }
    }
    
    

    3.在 directives/index 中绑定该指令

    import permission from './permission'
    
    export default app => {
      app.directive('permission', permission)
    }
    
    

    4.在页面中,添加指令

    <el-button type="primary" @click="searchEvent"  v-permission="'/adminAuth/admintorList'">查询</el-button>
    

    五、总结

    那么到这里我们整个权限受控就算是全部完成了。

    整个这一大节中,核心就是 RBAC的权限受控体系 。围绕着 用户->角色->权限 的体系是现在在包含权限控制的系统中使用率最广的一种方式。

    那么怎么针对于权限控制的方案而言,除了文中提到的这种方案之外,其实还有很多其他的方案,大家可以在我们的话题讨论中踊跃发言,多多讨论。

    相关文章

      网友评论

        本文标题:Vue3写一个后台管理系统(4)RBAC权限受控体系的实现

        本文链接:https://www.haomeiwen.com/subject/lxhcrdtx.html