Springboot + vue前后端分离后台管理系统(十二) -- 动态角色菜单

本文介绍了如何在前后端分离的系统中实现基于角色的权限控制(RBAC),通过两种方式配置动态路由菜单:前端过滤渲染和后端统一配置。详细讲解了后端返回树形结构数据,前端如何清洗数据生成适配Vue的路由,并展示了代码实现。此外,还涵盖了用户角色配置、权限管理以及不同角色登录的效果。
摘要由CSDN通过智能技术生成

前言

后台管理系统是基于RBAC设计的,也就是说不同的角色应该拥有不同的资源访问权限,这篇就来实现这个功能

实现

方式一

vue-admin-template 只提供了基础的vue后台管理框架,权限管理模块没有引入。
vue-element-admin 相对完整的组件demo和权限模块。它的动态路由配置如下:

  • 前端配置完整的路由菜单
  • 请求后端api返回 用户拥有的菜单
  • 在前端过滤渲染呈现用户的菜单

方式二


由后端统一配置菜单,直接返回给前端渲染

优缺点

方式一:对于前后端分离多人协作开发比较友好,前端只需要提供路由菜单的编码,由后端配置给对应角色用户。前端人员不受后端开发的约束。

方式二:动态路由菜单直接由后端配置并返回,相对安全。但是前端人员的路由文件的存放和命名就和后端人员耦合度较高。

这个demo尝试使用方式二的方式来实现动态路由菜单,喜欢方式一的可以直接研究vue-element-admin项目

系统管理功能

用户界面


用户管理:新增/编辑用户信息,并可以给用户设置一个或多个角色。

角色 界面


角色管理:新增/编辑角色信息,并可以给角色设置菜单权限。

菜单界面


菜单管理:新增/编辑、管理所有的基础菜单,不同角色可以指定不同的菜单。

核心代码

  • 数据库菜单数据

  • 接口返回给前端的树形结构数据

"menus": [{
			"id": "5e9d2ef576424b61445388acc285da0f",
			"pId": "0",
			"weight": "10",
			"name": "系统管理",
			"path": "/sys-manage",
			"component": "",
			"code": "sys-manage",
			"hidden": "0",
			"createTime": null,
			"forbidden": null,
			"icon": "el-icon-s-operation",
			"permission": "",
			"updateTime": null,
			"type": "0",
			"children": [{
				"id": "e308c7f841d56b791af44fd6a0b6745f",
				"pId": "5e9d2ef576424b61445388acc285da0f",
				"weight": "10",
				"name": "用户管理",
				"path": "/users",
				"component": "sys/user/users",
				"code": "users",
				"hidden": "0",
				"createTime": null,
				"forbidden": null,
				"icon": "el-icon-user-solid",
				"permission": "",
				"updateTime": null,
				"type": "0"
			}, {
				"id": "72633ded69127f0974141bb5687e541d",
				"pId": "5e9d2ef576424b61445388acc285da0f",
				"weight": "20",
				"name": "角色管理",
				"path": "/roles",
				"component": "sys/role/roles",
				"code": "roles",
				"hidden": "0",
				"createTime": null,
				"forbidden": null,
				"icon": "el-icon-s-custom",
				"permission": "",
				"updateTime": null,
				"type": "0"
			}, {
				"id": "5aadb65fed8eca1761bcabe8bb6b9cf9",
				"pId": "5e9d2ef576424b61445388acc285da0f",
				"weight": "30",
				"name": "菜单管理",
				"path": "/menus",
				"component": "sys/menu/menus",
				"code": "menus",
				"hidden": "0",
				"createTime": null,
				"forbidden": null,
				"icon": "el-icon-menu",
				"permission": "",
				"updateTime": null,
				"type": "0"
			}]
		}, {
			"id": "afc533ce3f5e8611efaa3888e11a9265",
			"pId": "0",
			"weight": "20",
			"name": "开发者工具",
			"path": "/develop",
			"component": "",
			"code": "develop",
			"hidden": "0",
			"createTime": null,
			"forbidden": null,
			"icon": "fa fa-desktop",
			"permission": "",
			"updateTime": null,
			"type": "0",
			"children": [{
				"id": "00aa7dc71a0c0d0538300919f8f308e9",
				"pId": "afc533ce3f5e8611efaa3888e11a9265",
				"weight": "10",
				"name": "测试1",
				"path": "/test1",
				"component": "",
				"code": "111",
				"hidden": "0",
				"createTime": null,
				"forbidden": null,
				"icon": "fa fa-cutlery",
				"permission": "",
				"updateTime": null,
				"type": "0"
			}, {
				"id": "512369a39292357cf9624fddc8e83ec8",
				"pId": "afc533ce3f5e8611efaa3888e11a9265",
				"weight": "10",
				"name": "代码生成",
				"path": "/gen",
				"component": "",
				"code": "gen",
				"hidden": "0",
				"createTime": null,
				"forbidden": null,
				"icon": "fa fa-code",
				"permission": "",
				"updateTime": null,
				"type": "0"
			}]
		}]
  • 新增一个store路由模块来清洗数据,把后端数据库表里的数据清洗成route支持的结构
import { asyncRoutes, constantRoutes } from '@/router'
/* Layout */
import Layout from '@/layout'


/**
 * 处理拼装侧边栏菜单
 * @param routes asyncRoutes
 * @param menus
 */
export function filterAsyncRoutes(menus) {
    const res = []
    // 遍历一级菜单 需要component: Layout 布局
    menus.forEach(menu => {
        const tmp = {
            path: menu.path,
            name: menu.code,
            component: Layout,
            meta: {
                title: menu.name,
                icon: menu.icon
            },
            hidden: menu.hidden === '0' ? false : true
        }
        if (menu.children) {
            tmp.children = initChildren(menu.children)
        }
        res.push(tmp)
    })


    return res
}

// 递归遍历子菜单
function initChildren(children) {
    const rs = []

    children.forEach(child => {
        const temp = {
            path: child.path,
            name: child.code,
            meta: {
                title: child.name,
                icon: child.icon
            },
            hidden: child.hidden === '0' ? false : true,
            component: loadView(child.component)
        }

        if (child.children) {
            temp.children = initChildren(child.children)
        }

        rs.push(temp)
    })



    return rs
}

/**
 * 解决动态加载路由 报错:Cannot find module 'XXXX', 例如:Cannot find module '@/views/sys/role/roles' 
 * @param {*} view 
 */
export const loadView = (view) => {
    return (resolve) => require([`@/views/${view}`], resolve)
};


const state = {
    routes: [],
    addRoutes: []
}

const mutations = {
    SET_ROUTES: (state, routes) => {
        state.addRoutes = routes
        state.routes = constantRoutes.concat(routes)

    }
}

const actions = {
    generateRoutes({ commit }, menus) {
        return new Promise(resolve => {
            let accessedRoutes
            accessedRoutes = filterAsyncRoutes(menus)
            //404 页面,放在最后  否则会报错。这是框架里说得 还真只能放最后。。。
            accessedRoutes.push({ path: '*', redirect: '/404', hidden: true })
            commit('SET_ROUTES', accessedRoutes)
            resolve(accessedRoutes)
        })
    }
}



export default {
    namespaced: true,
    state,
    mutations,
    actions
}
  • 修改permission.js逻辑
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style

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

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

router.beforeEach(async (to, from, next) => {
  // start progress bar
  NProgress.start()

  // 白名单直接跳转 如果是登陆路由,清空用户token
  if (whiteList.indexOf(to.path) !== -1) {
    next()
  } else {
    //验证是否有token, 有token跳转, 无token或token失效跳转登录页
    const token = store.getters.token
    if (token) {
      const name = store.getters.name
      if (name) {
        next();
      } else {
        // 获取动态路由菜单
        try {
          const { menus } = await store.dispatch('user/getInfo')

          const accessRoutes = await store.dispatch('accessRoutes/generateRoutes', menus)

          // dynamically add accessible routes
          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 })
        } catch (error) {
          // remove token and go to login page to re-login
          await store.dispatch('user/resetToken')
          Message.error(error || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    } else {
      // await store.dispatch('user/resetToken')
      Message.error('登陆失效,请重新登陆')
      next(`/login?redirect=${to.path}`)
    }
  }
})

router.afterEach(() => {
  // finish progress bar
  NProgress.done()
})

效果

  • 配置角色菜单

  • 配置用户角色

  • 用不同角色用户进行登陆

user

admin

结尾

基础的代码太多,就没有贴出来,展示出来的都是功能的效果。

唠叨一下

拖延症犯了就不想写文章。。。

  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值