RuoYi菜单管理:动态路由权限配置详解
引言
在企业级应用开发中,权限管理是确保系统安全性的核心环节。RuoYi作为一款基于SpringBoot的权限管理系统,其菜单管理和动态路由权限配置机制设计精巧、功能强大。本文将深入剖析RuoYi的菜单权限体系,从数据结构到实现原理,为您全面解析动态路由权限配置的最佳实践。
菜单数据结构设计
SysMenu实体类核心字段
RuoYi的菜单系统采用层次化设计,SysMenu实体类定义了完整的菜单属性:
public class SysMenu extends BaseEntity {
private Long menuId; // 菜单ID
private String menuName; // 菜单名称
private Long parentId; // 父菜单ID
private String orderNum; // 显示顺序
private String url; // 菜单URL
private String target; // 打开方式
private String menuType; // 类型(M目录 C菜单 F按钮)
private String visible; // 菜单状态(0显示 1隐藏)
private String perms; // 权限字符串
private String icon; // 菜单图标
private List<SysMenu> children; // 子菜单列表
}
菜单类型说明
| 类型 | 代码 | 说明 | 用途 |
|---|---|---|---|
| 目录 | M | 用于组织菜单的容器 | 构建菜单层级结构 |
| 菜单 | C | 具体的功能页面 | 用户可访问的功能模块 |
| 按钮 | F | 页面内的操作按钮 | 控制细粒度权限 |
权限配置流程
1. 数据库表设计
RuoYi使用三张核心表管理菜单权限:
sys_menu:存储菜单基本信息sys_role:角色信息表sys_role_menu:角色菜单关联表
-- 查询用户菜单权限的SQL
SELECT DISTINCT m.menu_id, m.parent_id, m.menu_name, m.url, m.visible,
IFNULL(m.perms,'') as perms, m.target, m.menu_type, m.icon, m.order_num
FROM sys_menu m
LEFT JOIN sys_role_menu rm ON m.menu_id = rm.menu_id
LEFT JOIN sys_user_role ur ON rm.role_id = ur.role_id
LEFT JOIN sys_role ro ON ur.role_id = ro.role_id
WHERE ur.user_id = #{userId}
AND m.menu_type IN ('M', 'C')
AND m.visible = 0
AND ro.status = 0
ORDER BY m.parent_id, m.order_num
2. 动态权限加载机制
RuoYi采用Apache Shiro作为安全框架,通过自定义UserRealm实现动态权限加载:
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
SysUser user = ShiroUtils.getSysUser();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 管理员拥有所有权限
if (user.isAdmin()) {
info.addRole("admin");
info.addStringPermission("*:*:*");
} else {
// 动态查询用户角色和权限
Set<String> roles = roleService.selectRoleKeys(user.getUserId());
Set<String> menus = menuService.selectPermsByUserId(user.getUserId());
info.setRoles(roles);
info.setStringPermissions(menus);
}
return info;
}
动态路由实现原理
前端路由生成流程
菜单数据转换处理
在后端服务中,菜单数据经过多层处理:
- 数据查询:根据用户ID查询有权限的菜单
- 树形结构构建:将扁平数据转换为层次结构
- 权限过滤:过滤掉无权限的菜单项
- 排序处理:按照order_num进行排序
// 构建菜单树形结构的关键方法
public List<SysMenu> getChildPerms(List<SysMenu> list, int parentId) {
List<SysMenu> returnList = new ArrayList<SysMenu>();
for (Iterator<SysMenu> iterator = list.iterator(); iterator.hasNext();) {
SysMenu t = (SysMenu) iterator.next();
if (t.getParentId() == parentId) {
recursionFn(list, t); // 递归处理子菜单
returnList.add(t);
}
}
return returnList;
}
权限缓存与更新机制
Shiro权限缓存
RuoYi使用Shiro的缓存机制提升性能,同时提供缓存清理方法:
// 清理所有用户授权信息缓存
public static void clearAllCachedAuthorizationInfo() {
getUserRealm().clearAllCachedAuthorizationInfo();
}
// 在菜单增删改时调用缓存清理
@PostMapping("/add")
@ResponseBody
public AjaxResult addSave(@Validated SysMenu menu) {
// ... 业务逻辑
AuthorizationUtils.clearAllCachedAuthorizationInfo(); // 清理缓存
return toAjax(menuService.insertMenu(menu));
}
缓存更新策略
| 操作类型 | 缓存影响 | 处理方式 |
|---|---|---|
| 菜单新增 | 需要更新 | 立即清理权限缓存 |
| 菜单修改 | 需要更新 | 立即清理权限缓存 |
| 菜单删除 | 需要更新 | 立即清理权限缓存 |
| 角色权限变更 | 需要更新 | 立即清理权限缓存 |
前端菜单渲染
动态选项卡实现
前端通过createMenuItem函数动态创建菜单选项卡:
function createMenuItem(dataUrl, menuName, isRefresh) {
var topWindow = $(window.parent.document);
// 检查选项卡是否已存在
$('.menuTab', topWindow).each(function() {
if ($(this).data('id') == dataUrl) {
// 激活已存在的选项卡
$(this).addClass('active').siblings('.menuTab').removeClass('active');
if (isRefresh) {
refreshTab(); // 刷新选项卡内容
}
return false;
}
});
// 创建新的选项卡和iframe
var str = '<a href="javascript:;" class="active menuTab noactive" data-id="' + dataUrl + '">' + menuName + ' <i class="fa fa-times-circle"></i></a>';
var str1 = '<iframe class="RuoYi_iframe" src="' + dataUrl + '" frameborder="0" data-id="' + dataUrl + '" seamless></iframe>';
$('.menuTab', topWindow).removeClass('active');
$('.mainContent', topWindow).append(str1);
$('.menuTabs .page-tabs-content', topWindow).append(str);
}
权限控制实现
前端通过Thymeleaf的shiro标签进行权限控制:
<a class="btn btn-success" onclick="$.operate.add(0)" shiro:hasPermission="system:menu:add">
<i class="fa fa-plus"></i> 新增
</a>
<a class="btn btn-primary" onclick="$.operate.edit()" shiro:hasPermission="system:menu:edit">
<i class="fa fa-edit"></i> 修改
</a>
最佳实践与配置示例
1. 菜单配置示例
-- 系统管理目录
INSERT INTO sys_menu VALUES (1, '系统管理', 0, '1', '#', 'menuItem', 'M', '0', NULL, 'fa fa-gear', NOW(), 'admin', NULL, NULL);
-- 用户管理菜单
INSERT INTO sys_menu VALUES (2, '用户管理', 1, '1', '/system/user', 'menuItem', 'C', '0', 'system:user:view', 'fa fa-user', NOW(), 'admin', NULL, NULL);
-- 用户管理相关按钮权限
INSERT INTO sys_menu VALUES (3, '用户查询', 2, '1', '#', 'menuItem', 'F', '0', 'system:user:list', '#', NOW(), 'admin', NULL, NULL);
INSERT INTO sys_menu VALUES (4, '用户新增', 2, '2', '#', 'menuItem', 'F', '0', 'system:user:add', '#', NOW(), 'admin', NULL, NULL);
INSERT INTO sys_menu VALUES (5, '用户修改', 2, '3', '#', 'menuItem', 'F', '0', 'system:user:edit', '#', NOW(), 'admin', NULL, NULL);
2. 权限字符串规范
RuoYi采用统一的权限字符串格式:模块:功能:操作
system:user:view- 系统模块用户功能的查看权限system:user:add- 系统模块用户功能的新增权限system:user:edit- 系统模块用户功能的修改权限system:user:remove- 系统模块用户功能的删除权限
3. 动态路由配置建议
# 应用配置示例
ruoyi:
shiro:
user:
admin: true # 是否管理员账号
anonUrl: /login # 匿名访问URL
filterChainDefinitions: # 过滤链定义
/login = anon
/logout = logout
/static/** = anon
/druid/** = anon
/websocket = anon
/** = user
常见问题与解决方案
1. 权限缓存不更新
问题:修改菜单权限后,用户需要重新登录才能生效。
解决方案:
// 在菜单管理Controller中调用缓存清理
@PostMapping("/edit")
@ResponseBody
public AjaxResult editSave(@Validated SysMenu menu) {
// ... 业务逻辑
AuthorizationUtils.clearAllCachedAuthorizationInfo(); // 清理权限缓存
return toAjax(menuService.updateMenu(menu));
}
2. 菜单排序混乱
问题:菜单显示顺序不符合预期。
解决方案:确保每个菜单项的order_num字段正确设置,后端查询时按parent_id, order_num排序。
3. 按钮权限不生效
问题:前端按钮仍然显示,但点击无权限操作时报错。
解决方案:在前端页面中添加shiro权限校验:
<button shiro:hasPermission="system:user:add">新增用户</button>
性能优化建议
1. 缓存策略优化
// 使用Redis集中式缓存替代本地缓存
@Configuration
public class ShiroConfig {
@Bean
public CacheManager cacheManager() {
RedisCacheManager cacheManager = new RedisCacheManager();
cacheManager.setRedisTemplate(redisTemplate);
return cacheManager;
}
}
2. 数据库查询优化
-- 添加索引提升查询性能
CREATE INDEX idx_user_role ON sys_user_role(user_id);
CREATE INDEX idx_role_menu ON sys_role_menu(role_id);
CREATE INDEX idx_menu_parent ON sys_menu(parent_id);
CREATE INDEX idx_menu_visible ON sys_menu(visible);
3. 前端懒加载优化
对于大型菜单系统,可以采用分页加载或懒加载策略:
// 实现菜单懒加载
function loadMenuLazily(parentId, callback) {
$.get('/system/menu/lazy?parentId=' + parentId, function(result) {
if (result.code === 200) {
callback(result.data);
}
});
}
总结
RuoYi的菜单管理和动态路由权限配置体系提供了一个完整、灵活且高效的权限管理解决方案。通过深入理解其设计原理和实现机制,开发者可以:
- 快速构建复杂的权限管理系统
- 灵活配置多层次的菜单结构
- 精细控制到按钮级别的权限
- 动态更新权限配置而不需要重启应用
- 优化性能通过合理的缓存策略
该体系的核心优势在于其松耦合的设计,菜单数据、角色权限、用户关联相互独立,使得系统具有良好的扩展性和维护性。无论是小型项目还是大型企业应用,RuoYi的权限管理系统都能提供可靠的安全保障和良好的用户体验。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



