在后台管理系统中,不同用户角色往往拥有不同的操作权限,对应的菜单展示也需动态调整。动态路由加载正是解决这一问题的核心方案 —— 根据登录用户的权限,从数据库查询其可访问的菜单,封装成前端所需的路由结构并返回。本文将详细讲解如何基于 Spring Boot + MyBatis-Plus 实现这一功能,包含完整代码与实现思路。
一、需求与实现思路
动态路由加载的核心目标是:根据登录用户的权限,动态生成其可访问的菜单路由,最终返回给前端用于渲染侧边栏。整体实现思路分为四步:
- 获取当前登录用户信息:通过 Session 获取已登录用户的 ID(userId);
- 查询用户角色名称:基于 userId,通过
user_role
表(用户 - 角色关联)和role
表(角色表)联查,获取用户的角色名称(如 “超级管理员”); - 查询用户权限菜单:基于 userId,通过
user_role
、role_menu
(角色 - 菜单关联)、menu
(菜单表)三表联查,获取用户可访问的所有菜单; - 封装路由结构:将数据库查询的菜单列表,转换为前端所需的路由格式(包含一级菜单、二级菜单、路由元信息等)。
二、核心表结构设计
实现动态路由的前提是合理的表结构设计,需包含 3 张核心表(用户 - 角色 - 菜单的关联关系):
user
:用户表(存储用户 ID、用户名等);role
:角色表(存储角色 ID、角色名称,如 “超级管理员”);menu
:菜单表(存储菜单 ID、父级 ID、路径、组件路径等路由信息);user_role
:用户 - 角色关联表(多对多关系);role_menu
:角色 - 菜单关联表(多对多关系)。
其中,menu
表的核心字段如下(与代码对应):
字段名 | 含义说明 | 示例值 |
---|---|---|
menu_id | 菜单 ID(主键) | 1 |
parent_id | 父级菜单 ID(0 表示一级菜单) | 0 |
name | 菜单名称(用于前端显示) | "系统管理" |
path | 路由路径 | "/sys" |
component | 前端组件路径 | "Layout" |
icon | 菜单图标(前端显示) | "system" |
hidden | 是否隐藏("true"/"false") | "false" |
sort | 排序号(控制菜单展示顺序) | 1 |
三、VO 类设计(适配前端路由格式)
前端路由通常需要包含菜单名称、路径、组件、图标等信息,且需区分一级菜单和子菜单。因此,我们设计以下 VO(View Object)类封装路由数据:
1. MenuRouterVO(一级菜单路由)
@Data
public class MenuRouterVO {private String name; // 菜单名称private String path; // 路由路径private String component; // 前端组件路径private String hidden; // 是否隐藏("true"/"false")private String redirect = "noRedirect"; // 重定向路径(默认无)private Boolean alwaysShow = true; // 是否总是显示(一级菜单通常为true)private MetaVO meta; // 路由元信息(包含标题、图标)private List<ChildMenuRouterVO> children; // 子菜单列表
}
2. ChildMenuRouterVO(二级菜单路由)
@Data
public class ChildMenuRouterVO {private String name; // 子菜单名称private String path; // 子菜单路径private String component; // 子菜单组件路径private String hidden; // 是否隐藏private MetaVO meta; // 子菜单元信息
}
3. MetaVO(路由元信息)
用于存储前端渲染所需的标题和图标:
@Data
public class MetaVO {private String title; // 菜单标题(显示在侧边栏)private String icon; // 菜单图标(如"system")
}
四、核心代码实现
1. 控制器:处理动态路由请求(Controller)
控制器的作用是接收前端请求,协调获取用户信息、角色、菜单,并封装返回结果。
@RestController
@RequestMapping("/sys/user")
public class UserController {@Autowiredprivate RoleMapper roleMapper;@Autowiredprivate MenuService menuService;/*** 加载动态路由:返回用户信息、角色、可访问菜单路由*/@GetMapping("/getRouters")public Result getRouters(HttpSession session) {// 1. 从Session获取当前登录用户(登录时已存入Session)User user = (User) session.getAttribute("user");if (user == null) {return Result.error("用户未登录");}// 2. 根据userId查询角色名称(如"超级管理员")String roleName = roleMapper.getRoleNameByUserId(user.getUserId());// 3. 根据userId查询并封装用户可访问的菜单路由List<MenuRouterVO> routers = menuService.getMenuRouterByUserId(user.getUserId());// 4. 封装结果返回(用户信息、角色、路由)return Result.ok().put("data", user) // 用户基本信息.put("roles", roleName) // 角色名称.put("routers", routers); // 动态路由列表}
}
2. 角色查询:获取用户角色名称(RoleMapper)
通过user_role
表关联role
表,根据 userId 查询角色名称:
@Repository
public interface RoleMapper extends BaseMapper<Role> {/*** 根据userId查询角色名称* 联表逻辑:user_role(用户-角色关联) → role(角色表)*/@Select("SELECT role_name FROM role, user_role " +"WHERE user_role.role_id = role.role_id " +"AND user_role.user_id = #{userId}")String getRoleNameByUserId(Integer userId);
}
说明:若用户拥有多个角色,可修改 SQL 为GROUP_CONCAT(role_name)
并返回字符串(如 “管理员,编辑”)。
3. 菜单查询:获取用户权限菜单(MenuMapper)
通过user_role
、role_menu
、menu
三表联查,获取用户可访问的所有菜单:
@Repository
public interface MenuMapper extends BaseMapper<Menu> {/*** 根据userId查询可访问的菜单列表* 联表逻辑:user_role → role_menu → menu*/@Select({"SELECT m.menu_id, m.parent_id, m.name, m.path, m.component, " +"m.icon, m.hidden, m.sort " +"FROM user_role ur, role_menu rm, menu m " +"WHERE ur.role_id = rm.role_id " +"AND rm.menu_id = m.menu_id " +"AND ur.user_id = #{userId} " +"ORDER BY m.sort" // 按sort排序,保证菜单展示顺序})List<Menu> getMenusByUserId(Integer userId);
}
说明:查询结果包含菜单的 ID、父级 ID、路径等核心信息,后续将转换为路由 VO。
4. 菜单服务:封装路由结构(MenuService)
Service 层的核心是将数据库查询的Menu
列表转换为前端所需的MenuRouterVO
列表,实现步骤:
- 从数据库查询用户可访问的所有菜单(
menuList
); - 筛选一级菜单(
parent_id = 0
); - 为每个一级菜单封装
MenuRouterVO
属性(名称、路径、组件等); - 为每个一级菜单匹配子菜单(
parent_id = 一级菜单ID
),封装为ChildMenuRouterVO
; - 组合一级菜单与子菜单,返回最终路由列表。
@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService {@Autowiredprivate MenuMapper menuMapper;@Overridepublic List<MenuRouterVO> getMenuRouterByUserId(Integer userId) {// 1. 查询用户可访问的所有菜单List<Menu> menuList = menuMapper.getMenusByUserId(userId);// 2. 存储最终的路由列表(一级菜单)List<MenuRouterVO> routerList = new ArrayList<>();// 3. 遍历菜单列表,筛选一级菜单并封装for (Menu menu : menuList) {// 一级菜单:parent_id = 0if (menu.getParentId() == 0) {MenuRouterVO parentRouter = new MenuRouterVO();// 封装一级菜单基本属性parentRouter.setName(menu.getName());parentRouter.setPath(menu.getPath());parentRouter.setComponent(menu.getComponent());parentRouter.setHidden(menu.getHidden());parentRouter.setRedirect("noRedirect"); // 固定值(前端要求)parentRouter.setAlwaysShow(true); // 总是显示一级菜单// 封装元信息(标题、图标,用于前端渲染)MetaVO parentMeta = new MetaVO();parentMeta.setTitle(menu.getName());parentMeta.setIcon(menu.getIcon());parentRouter.setMeta(parentMeta);// 4. 为当前一级菜单匹配子菜单List<ChildMenuRouterVO> children = new ArrayList<>();for (Menu childMenu : menuList) {// 子菜单:parent_id = 一级菜单IDif (childMenu.getParentId().equals(menu.getMenuId())) {ChildMenuRouterVO childRouter = new ChildMenuRouterVO();// 封装子菜单属性childRouter.setName(childMenu.getName());childRouter.setPath(childMenu.getPath());childRouter.setComponent(childMenu.getComponent());childRouter.setHidden(childMenu.getHidden());// 子菜单元信息MetaVO childMeta = new MetaVO();childMeta.setTitle(childMenu.getName());childMeta.setIcon(childMenu.getIcon());childRouter.setMeta(childMeta);children.add(childRouter);}}// 5. 绑定子菜单到一级菜单parentRouter.setChildren(children);routerList.add(parentRouter);}}return routerList;}
}
五、关键逻辑解析
1. 表关联查询的意义
动态路由的核心是 “权限控制”,而权限控制的基础是用户 - 角色 - 菜单的关联关系:
- 用户(user)通过
user_role
关联角色(role); - 角色(role)通过
role_menu
关联菜单(menu); - 最终实现 “用户→角色→菜单” 的权限传递,确保用户只能访问其角色允许的菜单。
2. 路由封装的核心思路
数据库查询的menuList
是扁平的菜单列表(包含一级和二级菜单),需要转换为树形结构(一级菜单包含子菜单列表):
- 先筛选
parent_id = 0
的一级菜单; - 再遍历所有菜单,为每个一级菜单匹配
parent_id
等于其menu_id
的子菜单; - 通过
MetaVO
封装前端渲染所需的标题和图标,确保与前端路由组件属性对应。
3. 扩展性考虑
若系统需要支持三级及以上菜单,只需修改 Service 层的封装逻辑,将子菜单的筛选改为递归处理:
// 递归获取子菜单(示例伪代码)
private List<ChildMenuRouterVO> getChildRouters(Integer parentId, List<Menu> menuList) {List<ChildMenuRouterVO> children = new ArrayList<>();for (Menu menu : menuList) {if (menu.getParentId().equals(parentId)) {ChildMenuRouterVO child = new ChildMenuRouterVO();// 封装子菜单属性...// 递归查询当前子菜单的子菜单(三级菜单)child.setChildren(getChildRouters(menu.getMenuId(), menuList)); children.add(child);}}return children;
}
六、最终返回结果示例
前端接收的 JSON 格式如下(与 VO 类结构对应),可直接用于渲染动态路由:
{"code": 200,"msg": "操作成功","data": {"userId": 1,"username": "admin","realName": "管理员"// ...其他用户信息},"roles": "超级管理员","routers": [{"name": "系统管理","path": "/sys","component": "Layout","hidden": "false","redirect": "noRedirect","alwaysShow": true,"meta": {"title": "系统管理","icon": "system"},"children": [{"name": "管理员管理","path": "/user","component": "sys/user/index","hidden": "false","meta": {"title": "管理员管理","icon": "user"}}]}]
}