1. 目标
实现对用户角色菜单的动态控制,即通过在管理端修改角色拥有的菜单和修改用户拥有的角色来控制用户看到的菜单。而路由和菜单信息则放在数据库中交给后台维护。(前端代码是在gitee.com/panjiachen/…基础上修改,感谢开源)
展示效果:
该管理员用户拥有的角色菜单如下
当修改管理员的角色所拥有的菜单之后
再次登录
2. 用户获取菜单流程
这里核心步骤就是第5步和第7步,对于后端而言要提供用户,角色,菜单配置等接口。并且提供用户管理,角色管理,菜单管理等功能
3. 功能和表设计
3.1. 功能设计
3.1.1. 用户管理
查看角色
角色分配
3.1.2. 角色管理
角色分配菜单,这里在开发时要考虑Element-ui树组件的选中和半选中状态,我在后面sys_role_menu的表设计中是新增一个标志位去区分。
3.1.3. 菜单管理
3.2. 后端表设计
对于后端而言要根据RBAC(基于角色权限的访问控制)设计表结构,这里按照最简单的5张表设计。这样可以很大程度将用户和角色和菜单信息解耦,对于开发过程十分友好。
| 表 | 作用 | 备注 |
|---|---|---|
| sys_user | 用户表 | |
| sys_role | 角色表 | |
| sys_menu | 菜单表 | |
| sys_user_role | 用户角色关联 | |
| sys_role_menu | 角色菜单关联 |
其中sys_menu表存储了菜单和路由的所有信息,是设计动态菜单和和路由的关键。而sys_user_role和sys_role_menu分别实现用户和角色,角色和菜单的多对多关系,在实际开发中建议对sys_user_role的user_id和role_id,sys_role_menu中的role_id和menu_id建立唯一约束。
-- jtool.sys_menu definition
CREATE TABLE `sys_menu` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT NULL,
`url` varchar(100) DEFAULT NULL,
`title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '显示名称',
`redirect` varchar(100) DEFAULT NULL,
`icon` varchar(100) DEFAULT NULL,
`sortno` varchar(100) DEFAULT NULL,
`parent_id` bigint DEFAULT NULL,
`menu_code` varchar(100) DEFAULT NULL,
`level` varchar(100) DEFAULT NULL,
`param` varchar(100) DEFAULT NULL,
`component_path` varchar(100) DEFAULT NULL COMMENT '组件路径',
`enabled` tinyint(1) DEFAULT '1' COMMENT '状态',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- jtool.sys_role definition
CREATE TABLE `sys_role` (
`ID` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`ROLE_TYPE` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '角色类型,是接口,或者菜单',
`ROLE_NAME` varchar(255) DEFAULT NULL COMMENT '角色名称',
`ROLE_CODE` varchar(255) DEFAULT NULL COMMENT '角色编码',
`CREATED_BY` bigint DEFAULT NULL COMMENT '创建人',
`CREATED_TIME` date DEFAULT NULL COMMENT '创建时间',
`UPDATED_BY` bigint DEFAULT NULL COMMENT '更新人',
`UPDATED_TIME` date DEFAULT NULL COMMENT '更新时间',
`DESCRIPTION` varchar(100) DEFAULT NULL,
PRIMARY KEY (`ID`),
UNIQUE KEY `role_un` (`ROLE_CODE`)
) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色';
-- jtool.sys_role_menu definition
CREATE TABLE `sys_role_menu` (
`id` bigint NOT NULL AUTO_INCREMENT,
`role_id` bigint DEFAULT NULL COMMENT '角色ID',
`menu_id` bigint DEFAULT NULL COMMENT '菜单ID',
`half_checked` tinyint(1) DEFAULT NULL COMMENT '菜单节点是否半选状态,0:否:1是',
PRIMARY KEY (`id`),
UNIQUE KEY `sys_role_menu_un` (`role_id`,`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1884487710487003328 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- jtool.sys_user definition
CREATE TABLE `sys_user` (
`ID` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`PASSWORD` varchar(255) DEFAULT NULL COMMENT '密码',
`USERNAME` varchar(255) DEFAULT NULL COMMENT '姓名',
`CREATED_BY` bigint DEFAULT NULL COMMENT '创建人',
`CREATED_TIME` date DEFAULT NULL COMMENT '创建时间',
`UPDATED_BY` bigint DEFAULT NULL COMMENT '更新人',
`UPDATED_TIME` date DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`ID`),
UNIQUE KEY `j_user_USERNAME_IDX` (`USERNAME`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户';
-- jtool.sys_user_role definition
CREATE TABLE `sys_user_role` (
`ID` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`USER_ID` bigint DEFAULT NULL COMMENT '用户ID',
`ROLE_ID` bigint DEFAULT NULL COMMENT '角色ID',
`ROLE_TYPE` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '角色类型,AUTHORITY或者MENU',
`CREATED_BY` bigint DEFAULT NULL COMMENT '创建人',
`CREATED_TIME` date DEFAULT NULL COMMENT '创建时间',
`UPDATED_BY` bigint DEFAULT NULL COMMENT '更新人',
`UPDATED_TIME` date DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`ID`),
UNIQUE KEY `sys_user_role_un` (`USER_ID`,`ROLE_ID`,`ROLE_TYPE`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户角色映射';
4. 前端动态路由
4.1. vue-router以及addRoute()
动态路由的设计过程是预先初始化一部分路由(比如登录页和404),然后使用vue-router提供的addRoutes(已经废弃)或者addRoute方法动态添加数据,如果vue-router没有addRoute方法可以考虑升级vue-router,作者使用的版本为3.5.1。
通过打印router可以看到路由信息。
console.log(router.options.routes)
这里的routes则是创建router时初始化提供的routes,如果是后来通过router.addRoutes()或者addRoute()函数添加的动态路由是不会在这里显示的,但实际是添加成功的,可以使用路由跳转测试添加的路由
router.push({path:"/test"})
详细的回答在这个issue中
有些管理系统菜单的实现是通过获取router.options.routes递归处理。但是对于动态路由而言,由于动态添加的routes不会在router.options.routes显示,那么菜单的渲染也无法完成。
另外有意思的是,在没有看到上面的issue回答之前,笔者以为router.options.routes里面没有新添加的路由就是没有添加成功,并且遇到这个问题的人不在少数,网上有很多相关问题的解答
4.2. 动态路由详细设计
在前置条件vue-router的addRoute实际上是添加成功之后并且router.options.routes无法显示后来添加的路由,于是左侧菜单的渲染不再借助router.options.routes,而是vuex。
5. 不足之处
- 暂时未测试深层次的菜单嵌套,可能会出现问题
- 菜单管理仅支持修改菜单名称,标题等,图标修改等功能暂时不支持;暂时不支持新增,删除
- 菜单表结构以及数据存储的设计不太友好,需要再考虑下
- 仅对粗放的页面权限控制,并未在接口层面控制