vue后台项目基于RBAC实现权限管理

Posted 奥特曼 

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue后台项目基于RBAC实现权限管理相关的知识,希望对你有一定的参考价值。

目录

一、RBAC(Role-Based Access control)

二、3个结构预备

(1)员工页:

(2)角色管理页:

(3)权限管理页:

三、实现RBAC权限设计模式

(1)给员工(用户)分配角色

(2)给角色(职位)分配权限

(3)实现动态生成左侧菜单栏(重点)

(4)实现页面中控制按钮权限(重点)

总结 

BUG

(1)刷新页面时出现404

(2)对于addRoute添加的路由,在刷新时会白屏

 (3)退出登录时重置路由

全部代码  gitee


一、RBAC(Role-Based Access control)

RBAC中文意为基于角色的权限控制,是一个权限设计思想,也就是说根据不同的用户角色,有不同的权限控制,例如权限页面,权限功能。

使用场景:登录后每个用户看到的菜单界面是不同的,操作的功能也不一样,例如老板、财务和员工,老板的级别就高级一点了,所有的菜单栏都可以访问,而财务只能看见考勤和对应的工资,而员工只能看见自己的个人信息。

这时候就产生了三个关系,分为用户、角色、权限点

用户:每个账号的人

角色:在公司中什么位置映射这有什么权限

权限点:每个角色可以操作什么

例如:用户===》老板===》所有权限,给每个用户一个角色定然有不同的权限点 ,用户和角色是一对多的关系

二、3个结构预备

(1)员工页:

功能:增、删、改、查、分页、分配角色 、导入表格导出表格

点击分配角色 弹出 分配角色界面

(2)角色管理页:

功能:新增角色(职位)、分配权限、编辑、删除、分页

(3)权限管理页:

一级表格代表页面级,二级表格代表按钮级

功能:对页面进行增(一定是有路由的)删改查

添加权限弹框 权限标识作为唯一标识

三、实现RBAC权限设计模式

下面两步基本的ajax步骤只是对操作进行概述,不放无用代码

(1)给员工(用户)分配角色

用户和角色是一对多的关系:一个用户可以拥有多个角色,这样他就会具体这多个角色的权限了。

(2)给角色(职位)分配权限

为什么要给角色设置职位?

1.从下图中可以看到,董事长所有的页面和所有的权限按钮都有权限,而员工只在薪资管理和考勤管理页面上有查看的权限,并没有其他的一些权限

2.给用户不同的身份,就代表这有什么权限

 ​​​​​​

例如给员工角色设置权限,从下图来看只能访问员工管理、考勤、工资页,其他页面是不能看到的,包括按钮级别导出表格用户是可以使用的但是没有导入表格的权限功能,我们如何进行实现呢?

(3)实现动态生成左侧菜单栏(重点)

在我们之前左侧的菜单栏都是固定死的,这就导致了每个用户访问的都是看到一样的页面,接下来我们就要使用vue-router对象中的addRoutes,用它去添加动态路由

addRoutes基本使用

router.addRoutes([路由配置对象])
或者:
this.$router.addRoutes([路由配置对象])

01、删掉动态路由

在之前我们都是静态和动态路由放在一起的,现在要删掉动态路由,保留静态路由

const createRouter = () => new Router({
  // mode: 'history', // require service support
  scrollBehavior: () => ({ y: 0 }),
  // routes: constantRoutes
  // 合并动态和静态的路由  , ...asyncRoutes
- routes: [...constantRoutes, ...asyncRoutes]
+ routes: [...constantRoutes]
})

02、 在前置路由守卫中添加路由

引入需要的动态路由,并动态添加路由,添加路由后,现在路由是可以访问的,但是菜单栏是只有静态路由了

// 引入所有的动态路由表(未经过筛选)  
// asyncRoutes 动态路由
+ import router, { asyncRoutes } from '@/router'

const whiteList = ['/login', '/404']
router.beforeEach(async(to, from, next) => {
  // 开启进度条
  NProgress.start()
  // 获取本地token 全局getter
  const token = store.getters.token
  if (token) {
    // 有token
    if (to.path === '/login') {
      next('/')
    } else {
      if (!store.getters.userId) {
        await store.dispatch('user/getUserInfo')
        // 改写成动态添加的方式
+       router.addRoutes(asyncRoutes)
      }
      next()
    }
  } else {
    // 没有token
    if (whiteList.includes(to.path)) {
      next()
    } else {
      next('/login')
    }
  }
  // 结束进度条
  NProgress.done()
})

03、定义vuex 动态生成左侧菜单-改写菜单保存位置

在我们之前是通过this.$router.options.routes拿到路由数据并渲染菜单栏,这个数据是固定的,我们通过addRoutes添加的路由只存在内存中并不会改变this.$router.options.routes拿到的值,所以现在要动态路由放到vuex中,因为vuex是响应式的,只要调用vuex,每个用户登录时得到路由数据的时候,都会去调用一次vuex   

补充模块。在src/store/modules/menu.js

// 导入静态路由
import { constantRoutes } from '@/router'
export default {
  namespaced: true,
  state: {
    // 先以静态路由作为菜单数据的初始值
    menuList: [...constantRoutes]
  },
  mutations: {
    setMenuList(state, asyncRoutes) {
      // 将动态路由和静态路由组合起来
      state.menuList = [...constantRoutes, ...asyncRoutes]
    }
  }
}

注册menu的vuex

+ import menu from './modules/menu'

Vue.use(Vuex)

const store = new Vuex.Store({
  modules: {
    app,
    settings,
    user,
+   menu
  },
  getters
})

04、在前置守卫中调用vuex

if (!store.getters.userId) {
        await store.dispatch('user/getUserInfo')
        // 动态添加可以访问的路由设置
        router.addRoutes(asyncRoutes)
        // 根据用户实际能访问几个页面来决定从整体8个路由设置
        // 中,过滤中出来几个,然后保存到vuex中
+       store.commit('menu/setMenuList', asyncRoutes)
      }

05、左侧菜单改写成 拿vuex中路由的数据

routes() {
  // 拿到的是一个完整的包含了静态路由和动态路由的数据结构
  // return this.$router.options.routes  拿所有路由改为拿vuex中的所有路由
  return this.$store.state.menu.menuList
}

06、过滤左侧菜单

目前通过05已经拿到了路由 左侧菜单栏目前显示了所有的路由,接下来就要去过滤路由,在我们登录的时候vuex中的user模块会返回给我们权限的数据,我们可以通过得到的数据去和路由对比,如果登录时返回的数据有这个路由就进行显示否则不显示

返回actions中登录信息的menus数据 

// 用来获取用户信息的action
    async getUserInfo(context) {
      // 1. ajax获取基本信息,包含用户id
      const rs = await getUserInfoApi()
      console.log('用来获取用户信息的,', rs)
      // 2. 根据用户id(rs.data.userId)再发请求,获取详情(包含头像)
      const info = await getUserDetailById(rs.data.userId)
      console.log('获取详情', info.data)
      // 把上边获取的两份合并在一起,保存到vuex中
      context.commit('setUserInfo', { ...info.data, ...rs.data })
      // 当前用户可以看到的菜单 res.data.roles.menus
+     return rs.data.roles.menus
    },

07、在路由前置中拿值并过滤

所有的路由去和登录时返回的数据进行过滤

if (!store.getters.userId) {
        // 有token,要去的不是login,就直接放行
        // 进一步获取用户信息
        // 发ajax---派发action来做
+       const menus = await store.dispatch('user/getUserInfo')
        console.log('当前用户能访问的页面', menus) // ['salarys', 'settings']
        console.log('当前系统功能中提供的所有的动态路由页面是', asyncRoutes)
        // 根据本用户实际的权限menus去 asyncRoutes 中做过滤,选出本用户能访问的页面

+       const filterRoutes = asyncRoutes.filter(route => {
+         const routeName = route.children[0].name
+         return menus.includes(routeName)
+       })

        // 一定要在进入主页之前去获取用户信息

        // addRoutes用来动态添加路由配置
        // 只有在这里设置了补充了路由配置,才可能去访问页面
        // 它们不会出现左侧
+       router.addRoutes(filterRoutes)

        // 把它们保存在vuex中,在src\\layout\\components\\Sidebar\\index.vue
        // 生成左侧菜单时,也应该去vuex中拿
+       store.commit('menu/setMenuList', filterRoutes)
      }

08 测试

页面权限设置成功了,换个角色为员工的账号进行查看,发现已经成功了

(4)实现页面中控制按钮权限(重点)

回头看一下刚才的图 在用户登录时会返回给我们一个权限数 里包含着页面权限,按钮权限,我们可以通过数据进行是否要显示数据,

通过自定义指令去判断是否要显示这个按钮,获取vuex中的登录时返回的按钮级别权限信息得到要显示的按钮,如果有就不用管显示就好了,如果没有 移除自己 

补充 自定义指令绑定的值v-allow="message"   可以在自定义指令中的binding.value 拿到message的值

// 注册一个全局自定义指令 `v-allow`
Vue.directive('allow', {
  inserted: function(el, binding) {
    // 从vuex中取出points,
    const points = store.state.user.userInfo.roles.points
    // 如果points有binding.value则显示
    if (points.includes(binding.value)) {
      // console.log('判断这个元素是否会显示', el, binding.value)
    } else {
      el.parentNode.removeChild(el)
      // el.style.display = 'none'
    }
  }
})

在按钮上绑定自定义指令

<el-button
+           v-allow="'import_employee'"
            type="warning"
            size="small"
            @click="$router.push('/import')"
          >导入excel</el-button>

总结 

RBAC(Role-Based Access control)一种基于角色的设计思想

  1. 给员工配置角色 (一个员工可以拥有多个角色)

  2. 给角色配置权限点 (一个角色可以有多个权限点)

员工只要有了角色之后,就自动拥有了角色绑定的所有权限点

根据权限设计思想对应业务模块

  1. 员工管理

  2. 角色管理

  3. 权限点管理(它是没有调整的余地的:它会严格与当前系统的功能对应!)

页面权限

菜单权限控制

登录 > 菜单权限数据 > 和本地的所有的动态路由数据做匹配出具 > 得到根据权限筛选之后的动态路由数据

1. 添加到路由系统中 (可以根据路径标识渲染组件 **addRoutes**)
2. 添加到左侧菜单渲染  (vuex管理 + v-for遍历)

按钮权限控制

登录 > 按钮权限数据 > 使用按钮单独的权限标识 去权限数据里面查找 ————通过自定义指令

BUG

(1)刷新页面时出现404

动态添加路由后 404的位置不是路由的末尾了 把404页面改到最末尾就好了

1. 从route/index.js中的静态路由中删除path:'*'这一项

// 不需要特殊的权限控制就可以访问的页面
export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  },
  
  // 404 page must be placed at the end !!!
-  { path: '*', redirect: '/404', hidden: true }
]

2.在路由守卫中最后添加到过滤的路由数组中

if (!store.getters.userId) {
// ....
  const filterRoutes = asyncRoutes.filter(route => {
    const routeName = route.children[0].name
    return menus.includes(routeName)
  })

  // 一定要在进入主页之前去获取用户信息

  // 把404加到最后一条
+  filterRoutes.push( // 404 page must be placed at the end !!!
    { path: '*', redirect: '/404', hidden: true })

  // addRoutes用来动态添加路由配置
  // 只有在这里设置了补充了路由配置,才可能去访问页面
  // 它们不会出现左侧
  router.addRoutes(filterRoutes)
// ...
} 

(2)对于addRoute添加的路由,在刷新时会白屏

if (!store.getters.userId) {
  // 省略其他...
  
  
  
  
  // 解决刷新出现的白屏bug
  next({
    ...to, // next({ ...to })的目的,是保证路由添加完了再进入页面 (可以理解为重进一次)
    replace: true // 重进一次, 不保留重复历史
  })
} else {
  next()
}

 (3)退出登录时重置路由

退出后,再次登陆,发现菜单异常 (控制台有输出说路由重复)

原因:

路由设置是通过router.addRoutes(filterRoutes)来添加的,退出时,并没有清空,再次登陆,又加了一次,所以有重复。

需要将路由权限重置 (恢复默认) 将来登录后再次追加才可以,不然的话,就会重复添加

1.在router/index.js中定义一个方法  这个方法将路由重新实例化

// 重置路由
export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // 重新设置路由的可匹配路径
}

2.退出的时候调用这个方法

import { resetRouter } from '@/router'
// 退出的action操作
logout(context) {
  // 1. 移除vuex个人信息
  context.commit('removeUserInfo')
  // 2. 移除token信息
  context.commit('removeToken')
  // 3. 重置路由
  resetRouter()
}

全部代码  gitee

路由守卫  permission.js  

// 02 在路由守卫中引入所有动态路由
import router, { asyncRoutes } from './router'
import store from './store/index'
// import { Message } from 'element-ui'
// 进度条
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
// import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'

// NProgress.configure({ showSpinner: false }) // NProgress Configuration

// const whiteList = ['/login'] // no redirect whitelist

// 跳转路由标题
router.beforeEach((to, from, next) => {
  document.title = getPageTitle(to.meta.title)
  next()
})

const whiteList = ['/404', '/login']
router.beforeEach(async(to, from, next) => {
  const token = store.state.user.token
  console.log(to)
  if (token) {
    if (to.path === '/login') {
      next('/')
    } else {
      if (!store.state.user.userInfo.userId) {
        // 如果没有用户id 的时候再去发渲染请求
        const menu = await store.dispatch('user/getUserInfo')
        // 10拿到可以访问的页面
        console.log('用户可以访问的页面', menu)
        console.log('一共有的页面', asyncRoutes)
        // 11 过滤路由
        const filterRoutes = asyncRoutes.filter(route => {
          const name = route.children[0].name
          return menu.includes(name)
        })

        // 解决刷新404显示404页面
        filterRoutes.push(
          // 404 page must be placed at the end !!!
          { path: '*', redirect: '/404', hidden: true }
        )
        // 03 动态添加路由  现在左侧已经没有路由了 但是仍然可以直接访问
        // router.addRoutes(asyncRoutes)

        // 06 调用vuex 把动态路由放到vuex中
        // store.commit('menu/setMenuList', asyncRoutes)
        // 11 把过滤后的路由添加到动态利用  用来跳转路由
        router.addRoutes(filterRoutes)
        // 13 用来显示菜单栏
        store.commit('menu/setMenuList', filterRoutes)

        // 解决刷新出现的白屏bug
        next({
          ...to, // next({ ...to })的目的,是保证路由添加完了再进入页面 (可以理解为重进一次)
          replace: true // 重进一次, 不保留重复历史
        })
      } else {
        next()
      }
    }
  } else {
    // 因为是后台管理系统 不能访问别的页面
    whiteList.includes(to.path) ? next() : next('/login')
  }
  // 进度条开始
  NProgress.start()
})

router.afterEach(() => {
  // finish progress bar
//   进去页面之后进度条结束
  NProgress.done()
})

store/modules/user.js  actions部分

  async getUserInfo(context) {
      try {
        const user = await getInfo()
        const id = user.data.userId
        const userInfo = await getUserInfo(id)
        console.log(userInfo)
        context.commit('setUserInfo', { ...user.data, ...userInfo.data })
        // 09 user中包含哪些页面是可以访问的
        console.log(user)
        return user.data.roles.menus
      } catch (error) {
        throw new Error(error)
      }
    },

store/modules/menu.js

// 05 创建menu.js 把路由保存到vuex中
import { constantRoutes } from '@/router/index'
const state = {
  // 静态路由作为菜单的初始值
  menuList: [...constantRoutes]
}

const mutations = {
  setMenuList(state, asyncRoutes) {
    // 把路由和菜单组合起来
    state.menuList = [...constantRoutes, ...asyncRoutes]
  }
}

const actions = {

}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

以上是关于vue后台项目基于RBAC实现权限管理的主要内容,如果未能解决你的问题,请参考以下文章

基于Vue+AntDesign实现的JAVA前后端分离后台管理系统

vue基于d2-admin的RBAC权限管理解决方案

Laravel9+Vue+ElementUI实现RBAC权限管理系统

基于RBAC权限控制模型的管理系统的设计与实现

基于RBAC权限控制模型的管理系统的设计与实现

基于Vue+AntDesign实现的JAVA前后端分离后台管理系统