划重点:RBAC 后台管理系统权限控制,Vue3高级实现方案

在后台管理系统中,权限控制是必不可少的,那么权限控制都包括什么呢?

  • 页面权限:菜单栏是否显示
  • 功能权限:按钮是否可点击

不同的账号显示的菜单栏不同,页面上可以操作的按钮也不一样,这背后就是依据RBAC模型来实现的

什么是RBAC

RBAC(Role-Based Access Control)模型是一种用于访问控制的权限管理模型。在 RBAC 模型中,权限的分配和管理是基于角色进行的。

RBAC 模型包含以下几个核心概念:

  1. 用户(User):用户是实际使用系统的人员或实体。每个用户都可以关联到一个或多个角色。
  2. 角色(Role):角色代表了一组具有相似权限需求的用户。每个用户可以被分配一个或多个角色,并通过角色来确定其拥有的权限。
  3. 权限(Permission):权限指定了对系统资源进行操作的能力。它们定义了用户在系统中可以执行的动作或访问的资源范围。系统中的菜单、接口、按钮都可以抽象为资源。

在 RBAC 模型中,管理员为每个角色分配适当的权限,然后将角色与用户关联起来,从而控制用户对系统资源的访问。这种角色与权限之间的层次结构和关系,使得权限管理更加灵活和可维护。如下图:

辅助业务

  1. 员工管理(用户列表)
    • 查看详情
    • 为用户分配角色
    • 删除用户操作
  2. 角色列表:
    • 为角色分配权限
  3. 权限列表

实现逻辑

  1. 页面权限 的核心是 路由表配置,路由表分为两部分:

    • 共有路由表(publicRoutes):每个角色都有的路由表,例如登录界面、404界面、401界面
    • 私有路由表(privateRoutes):不同角色拥有不同的路由表

    ▶️ 整个 页面权限 实现分为以下几步:

    • 获取 权限数据
    • 私有路由表 不再被直接加入到 routes
    • 利用 addRoute API 动态添加路由到 路由表
  2. 功能权限 的核心在于 根据数据隐藏功能按钮,隐藏的方式可以通过Vue的自定义指令进行控制

    ▶️ 整个 功能权限 实现分为以下几步:

    • 获取 权限数据
    • 定义 隐藏按钮方式(通过指令)
    • 依据数据隐藏按钮

页面权限代码实现

获取权限数据

在store中封装获取数据的方法,并存储用户数据,在src/permission中调用方法(在后面)

权限数据在 **userInfo -> permission -> menus**** 中**

javascript 复制代码
import { getUserInfo } from '@/api/sys'

export default {
  namespaced: true,
  state: () => ({
    userInfo: {}
  }),
  mutations: {
    ...
    setUserInfo(state, userInfo) {
      state.userInfo = userInfo
    }
  },
  actions: {
    ...
    async getUserInfo(context) {
      const res = await getUserInfo()
      this.commit('user/setUserInfo', res)
      return res
    },
		...
  }
}

接口返回数据如下图:

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

javascript 复制代码
/**
 * 私有路由表
 */
export const privateRoutes = []

/**
 * 公开路由表
 */
export var publicRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index')
  },
  {
    path: '/',
    component: layout,
    redirect: '/profile',
    children: [
      {
        path: '/profile',
        name: 'profile',
        component: () => import('@/views/profile/index'),
        meta: {
          title: 'profile',
          icon: 'personnel'
        }
      },
      {
        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
})

利用 addRoute API 动态添加路由到 路由表 中

  1. 创建 router/modules 文件夹,放入所有权限路由

为每个权限路由指定一个 name,每个 name 对应一个 页面权限name值是用来和获取到的数据进行匹配的

例如:RoleList.js,其余的不在此展示了

javascript 复制代码
import layout from '@/layout'

export default {
  path: '/user',
  component: layout,
  redirect: '/user/manage',
  
  name: 'roleList',
  
  meta: {
    title: 'user',
    icon: 'personnel'
  },
  children: [
    {
      path: '/user/role',
      component: () => import('@/views/role-list/index'),
      meta: {
        title: 'roleList',
        icon: 'role'
      }
    }
  ]
}
  1. router/index 中合并这些路由到 privateRoutes
javascript 复制代码
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
]
  1. 创建 store/modules/permission 模块,专门处理路由
javascript 复制代码
// 专门处理权限路由的模块
import { publicRoutes, privateRoutes } from '@/router'
export default {
  namespaced: true,
  state: {
    // 路由表:初始拥有静态路由权限
    routes: publicRoutes
  },
  mutations: {
    /**
     * 增加路由
     */
    setRoutes(state, newRoutes) {
      // 永远在静态路由的基础上增加新路由
      state.routes = [...publicRoutes, ...newRoutes]
    }
  },
  actions: {
    /**
     * 根据权限筛选路由
     */
    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
    }
  }
}
  1. src/permission 中,触发store中的getUserInfo获取用户数据,然后触发filterRoutes将匹配到的数据使用addRoute添加到路由表中
javascript 复制代码
import router from './router'
import store from './store'

const whiteList = ['/login']

/**
 * 路由前置守卫
 * to 要去哪里
 * from 当前导航正要离开的路由
 * next 往哪去
 */
router.beforeEach(async (to, from, next) => {
  if (store.getters.token) {
    if (to.path === '/login') {
      next('/')
    } else {
      // 判断用户信息是否获取
      // 若不存在用户信息,则需要获取用户信息
      if (!store.getters.hasUserInfo) {
        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()
    }
  } else {
    if (whiteList.indexOf(to.path) > -1) {
      next()
    } else {
      next('/login')
    }
  }
})

到这里页面权限的动态路由就完成了,但是更换用户后需要刷新页面,左侧菜单才会更新,原因就是:退出登录时,添加的路由表并未被删除

要解决这个问题,我们只需要在退出登录时,删除动态添加的路由表即可

(1)在 router/index 中定义 resetRouter 方法,使用removeRouteAPI 移除路由

javascript 复制代码
/**
 * 初始化路由表
 */
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) => {
      router.removeRoute(menu)
    })
  }

(2)在退出登录的动作下,触发该方法

功能权限代码实现

对于功能权限 ,我这里是通过这样格式的指令进行控制 v-permission="['importUser']"

  1. 获取权限数据在页面权限第一步已经完成,就是**userInfo -> permission -> points**
  2. 创建自定义指令 directives/permission
javascript 复制代码
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 中绑定该指令
javascript 复制代码
import permission from './permission'
export default app => {
  app.directive('permission', permission)
}

不要忘了在main.js中引入呦

javascript 复制代码
import installDirective from '@/directives'
...
installDirective(app)
  1. 在对应的按钮上使用指令即可,如下:
javascript 复制代码
<el-button
    ...
    v-permission="['distributeRole']"
>分配角色</el-button>

将自定义指令方法的参数打印出来

至此我们的权限控制就完成了,如有什么不足的地方,欢迎大家评论区留言。

相关推荐
清水白石0082 小时前
Vue.js 与 Flask/Django 后端配合:构建现代 Web 应用的最佳实践
vue.js
一只小阿乐9 小时前
前端web端项目运行的时候没有ip访问地址
vue.js·vue·vue3·web端
计算机学姐10 小时前
基于python+django+vue的旅游网站系统
开发语言·vue.js·python·mysql·django·旅游·web3.py
.ccl10 小时前
web开发 之 HTML、CSS、JavaScript、以及JavaScript的高级框架Vue(学习版2)
前端·javascript·vue.js
小徐不会写代码10 小时前
vue 实现tab菜单切换
前端·javascript·vue.js
2301_7653475411 小时前
Vue3 Day7-全局组件、指令以及pinia
前端·javascript·vue.js
辛-夷11 小时前
VUE面试题(单页应用及其首屏加载速度慢的问题)
前端·javascript·vue.js
刘志辉12 小时前
vue传参方法
android·vue.js·flutter
dream_ready13 小时前
linux安装nginx+前端部署vue项目(实际测试react项目也可以)
前端·javascript·vue.js·nginx·react·html5
编写美好前程13 小时前
ruoyi-vue若依前端是如何防止接口重复请求
前端·javascript·vue.js