尧图精选

SpringBoot+Vue动态路由菜单权限管理实践

🕒 发布时间:2026/9/16 17:03:16 📁 来源:尧图网络
1. SpringBoot与Vue.js实现动态路由菜单管理在前后端分离架构中动态路由菜单管理是权限系统的核心功能之一。最近我在重构一个后台管理系统时深入实践了基于SpringBoot和Vue.js的动态路由方案。与传统的静态路由配置不同这套方案允许管理员通过界面直接编辑菜单结构和权限系统会根据用户角色动态生成可访问的路由。2. 技术架构设计2.1 整体技术栈后端SpringBoot 2.7 MyBatis前端Vue 2.x Element UI数据库MySQL 8.0权限模型RBAC基于角色的访问控制2.2 动态路由实现原理动态路由的核心在于将路由配置从前端代码中抽离改为由后端接口返回。具体流程如下用户登录成功后前端请求/getRouters接口后端根据用户角色查询其拥有的菜单权限将菜单数据转换为Vue Router需要的格式返回前端通过router.addRoutes()动态添加路由这种设计使得菜单权限的变更无需重新部署前端项目真正实现了配置即生效。3. 后端实现详解3.1 数据库设计我们设计了5张核心表来实现RBAC模型3.1.1 菜单权限表(admin_menu_info)CREATE TABLE admin_menu_info ( menu_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 菜单ID, menu_name VARCHAR(50) NOT NULL DEFAULT COMMENT 菜单名称, menu_icon VARCHAR(50) NOT NULL DEFAULT COMMENT 菜单图标, menu_type TINYINT(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT 菜单类型1目录2菜单3按钮, menu_parent INT(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 父菜单ID, menu_visible TINYINT(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT 是否可见0隐藏 1显示, menu_status TINYINT(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT 菜单状态0停用 1使用, sort_order INT(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 排列顺序, routes_name VARCHAR(200) NOT NULL DEFAULT COMMENT 路由名称, routes_path VARCHAR(200) NOT NULL DEFAULT COMMENT 路由地址, routes_component VARCHAR(200) NOT NULL DEFAULT COMMENT 路由组件, routes_params VARCHAR(200) NOT NULL DEFAULT COMMENT 路由参数, routes_redirect VARCHAR(200) NOT NULL DEFAULT COMMENT 路由重定向, menu_perms VARCHAR(200) NOT NULL DEFAULT COMMENT 权限标示, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (menu_id) ) ENGINEINNODB DEFAULT CHARSETutf8mb4 COMMENT菜单权限表3.1.2 角色与关联表-- 角色表 CREATE TABLE admin_role_info ( role_id int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 角色ID, role_name varchar(50) NOT NULL DEFAULT COMMENT 角色名称, role_remark varchar(200) NOT NULL DEFAULT COMMENT 角色备注, role_status tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT 角色状态0停用 1使用, sort_order int(10) unsigned NOT NULL DEFAULT 0 COMMENT 排列顺序, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (role_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT角色表 -- 角色菜单关联表 CREATE TABLE admin_role_menu ( role_id int(10) unsigned NOT NULL DEFAULT 0 COMMENT 角色ID, menu_id int(10) unsigned NOT NULL DEFAULT 0 COMMENT 菜单ID, PRIMARY KEY (role_id,menu_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT角色菜单权限关联表3.1.3 用户与关联表-- 用户表 CREATE TABLE admin_user_info ( user_id int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 主键, user_truename varchar(50) NOT NULL DEFAULT COMMENT 姓名, user_avatar varchar(255) NOT NULL DEFAULT COMMENT 头像, user_name varchar(50) NOT NULL DEFAULT COMMENT 账号, pass_word varchar(50) NOT NULL DEFAULT COMMENT 密码, user_status tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT 用户状态0停用 1使用, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT管理员表 -- 用户角色关联表 CREATE TABLE admin_user_role ( user_id int(10) unsigned NOT NULL DEFAULT 0 COMMENT 用户ID, role_id int(10) unsigned NOT NULL DEFAULT 0 COMMENT 角色ID, PRIMARY KEY (user_id,role_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户角色关联表3.2 核心接口实现3.2.1 路由菜单接口RestController public class MenuController { GetMapping(/getRouters) public JsonResult getRouters(){ ListMenuResponse menuResponseList new ArrayList(); // 构建Demo菜单 MenuResponse menu new MenuResponse(); menu.setPath(/demo); menu.setName(Demo); menu.setComponent(Layout); menu.setRedirect(/demo/table); menu.setType(1); menu.setVisible(1); MenuMetaResponse meta new MenuMetaResponse(Demo菜单,el-icon-menu); menu.setMeta(meta); // 添加子菜单 ListMenuResponse children new ArrayList(); MenuResponse menu1 new MenuResponse(); menu1.setPath(table); menu1.setName(Table); menu1.setComponent(demo/table); menu1.setType(2); menu1.setVisible(1); MenuMetaResponse meta1 new MenuMetaResponse(表格页面); menu1.setMeta(meta1); children.add(menu1); menu.setChildren(children); menuResponseList.add(menu); return JsonResult.success(menuResponseList); } }3.2.2 接口返回示例{ msg: 操作成功, code: 200, data: [ { name: Demo, path: /demo, component: Layout, redirect: /demo/table, meta: { title: Demo菜单, icon: el-icon-menu }, type: 1, visible: 1, children: [ { name: Table, path: table, component: demo/table, meta: { title: 表格页面 }, type: 2, visible: 1 } ] } ] }4. 前端实现详解4.1 动态路由处理4.1.1 路由拦截器在router.beforeEach中处理动态路由router.beforeEach(async (to, from, next) { // 如果未登录跳转到登录页 if (!store.getters.token) { if (whiteList.includes(to.path)) { return next() } return next(/login?redirect${to.path}) } // 已登录状态下初始化动态路由 if (!store.getters.addRoutes.length) { try { const { menus } await store.dispatch(user/getInfo) const accessRoutes await store.dispatch(permission/generateRoutes, menus) router.addRoutes(accessRoutes) next({ ...to, replace: true }) } catch (error) { await store.dispatch(user/resetToken) Message.error(error || Has Error) next(/login?redirect${to.path}) } } else { next() } })4.1.2 路由转换将后端菜单数据转换为Vue路由配置export function filterAsyncRoutes(routes) { return routes.map(route { const { path, component, name, meta, children, redirect } route const routeItem { path, name, meta, redirect, component: component Layout ? Layout : loadView(component) } if (children children.length) { routeItem.children filterAsyncRoutes(children) } return routeItem }) } function loadView(view) { return () import(/views/${view}) }4.2 菜单管理界面4.2.1 菜单列表组件template div idtable-container el-table :datatableData row-keymenuId :tree-props{children: children, hasChildren: hasChildren} stripe border el-table-column propmenuId label菜单编号 width100/el-table-column el-table-column propmenuName label菜单名称/el-table-column el-table-column propmenuType label菜单类型 :formatterformatMenuType/el-table-column el-table-column proproutesPath label路由地址/el-table-column el-table-column label操作 template slot-scopescope el-button sizemini typeinfo iconel-icon-edit clickeditMenuDialog(scope.row.menuId)编辑/el-button el-button sizemini typedanger iconel-icon-delete clickdelMenuDialog(scope.row.menuId)删除/el-button /template /el-table-column /el-table menu-edit refmenuEditDialog :dialog-iddialogId :dialog-titledialogTitle :dialog-visibledialogVisible handleClosecloseMenuDialog submitFormsubmitMenuDialog / /div /template script import { getMenuList, removeMenu } from /api/auth_menu import MenuEdit from ./menu_edit.vue export default { components: { MenuEdit }, data() { return { dialogId: 0, dialogTitle: , dialogVisible: false, tableData: [] } }, created() { this.initMenuList() }, methods: { initMenuList() { getMenuList().then(res { this.tableData res.data }) }, editMenuDialog(id) { this.dialogId id this.dialogTitle 编辑菜单 this.dialogVisible true }, delMenuDialog(id) { this.$confirm(确定要删除这条记录吗, 确认删除, { type: error }).then(() { removeMenu(id).then(() { this.initMenuList() }) }) }, formatMenuType(row, column, cellValue) { const typeMap { 1: 目录, 2: 菜单, 3: 按钮 } return typeMap[cellValue] || 未知类型 } } } /script4.2.2 菜单编辑组件template el-dialog :titledialogTitle :visible.syncdialogVisible width800px el-form refform :modelform :rulesrules label-width100px el-form-item label菜单名称 propmenuName el-input v-modelform.menuName placeholder请输入菜单名称 / /el-form-item el-form-item label菜单类型 propmenuType el-radio-group v-modelform.menuType el-radio :label1目录/el-radio el-radio :label2菜单/el-radio el-radio :label3按钮/el-radio /el-radio-group /el-form-item el-form-item label路由组件 proproutesComponent el-input v-modelform.routesComponent placeholder如system/user/index / div classform-tip组件路径相对于views目录/div /el-form-item /el-form div slotfooter el-button clickdialogVisible false取消/el-button el-button typeprimary clicksubmitForm确定/el-button /div /el-dialog /template script import { getMenuInfo, addMenu, editMenu } from /api/auth_menu export default { props: [dialogId, dialogTitle, dialogVisible], data() { return { form: { menuId: 0, menuName: , menuType: 2, routesComponent: }, rules: { menuName: [{ required: true, message: 请输入菜单名称, trigger: blur }], menuType: [{ required: true, message: 请选择菜单类型, trigger: change }], routesComponent: [{ required: true, message: 请输入组件路径, trigger: blur }] } } }, watch: { dialogVisible(val) { if (val) { if (this.dialogId) { this.getDetail() } else { this.resetForm() } } } }, methods: { getDetail() { getMenuInfo(this.dialogId).then(res { this.form res.data }) }, resetForm() { this.form { menuId: 0, menuName: , menuType: 2, routesComponent: } }, submitForm() { this.$refs.form.validate(valid { if (valid) { const action this.form.menuId ? editMenu : addMenu action(this.form).then(() { this.$emit(submitForm) this.dialogVisible false }) } }) } } } /script5. 关键问题与解决方案5.1 路由缓存问题动态添加的路由在页面刷新后会丢失解决方案将路由信息存储在Vuex中在应用初始化时从Vuex恢复路由配合sessionStorage做持久化5.2 权限验证流程完整的权限验证应包含以下步骤登录时获取用户基本信息及token根据用户角色获取菜单权限将菜单转换为路由配置并动态添加生成侧边栏菜单每次路由跳转时验证权限5.3 按钮级权限控制对于页面中的操作按钮可以通过自定义指令实现权限控制Vue.directive(permission, { inserted(el, binding, vnode) { const { value } binding const permissions store.getters.permissions if (value !permissions.includes(value)) { el.parentNode el.parentNode.removeChild(el) } } }) // 使用方式 el-button v-permissionsystem:user:add新增/el-button6. 项目实践建议6.1 开发流程优化先在后端设计好菜单数据结构在前端创建对应的页面组件通过管理界面添加菜单配置测试路由跳转和权限控制6.2 安全性考虑后端需要对接口进行权限验证敏感操作需要记录日志密码等敏感信息需要加密传输定期审查权限分配6.3 性能优化对菜单接口添加缓存实现路由懒加载对频繁访问的页面添加keep-alive按需加载Element UI组件这套动态路由方案在实际项目中运行稳定能够满足大多数后台管理系统的权限需求。通过将路由配置数据化极大提高了系统的灵活性和可维护性。对于更复杂的权限场景可以在此基础上扩展数据权限和操作权限的控制。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →