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

一、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的权限受控体系 。围绕着 用户->角色->权限 的体系是现在在包含权限控制的系统中使用率最广的一种方式。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容