ruoyi-vue-pro路由管理:动态路由与权限控制
在企业级后台管理系统中,路由管理和权限控制是核心功能模块。ruoyi-vue-pro作为一款基于Spring Boot + Vue的前后端分离架构项目,提供了完整的动态路由和精细化权限控制解决方案。本文将深入解析其实现原理和最佳实践。
🎯 核心架构设计
ruoyi-vue-pro采用RBAC(Role-Based Access Control,基于角色的访问控制)模型,实现了菜单、按钮级别的精细化权限控制。整体架构如下图所示:
权限模型设计
🔐 后端权限控制实现
1. 权限服务核心逻辑
ruoyi-vue-pro的后端权限控制通过PermissionServiceImpl类实现,主要包含以下核心方法:
// 判断用户是否拥有任意权限
@Override
public boolean hasAnyPermissions(Long userId, String... permissions) {
if (ArrayUtil.isEmpty(permissions)) return true;
// 获取用户有效角色
List<RoleDO> roles = getEnableUserRoleListByUserIdFromCache(userId);
if (CollUtil.isEmpty(roles)) return false;
// 遍历权限判断
for (String permission : permissions) {
if (hasAnyPermission(roles, permission)) return true;
}
// 超管权限检查
return roleService.hasAnySuperAdmin(convertSet(roles, RoleDO::getId));
}
// 角色菜单权限分配
@Override
@DSTransactional
@Caching(evict = {
@CacheEvict(value = RedisKeyConstants.MENU_ROLE_ID_LIST, allEntries = true),
@CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, allEntries = true)
})
public void assignRoleMenu(Long roleId, Set<Long> menuIds) {
// 计算新增和删除的菜单
Set<Long> dbMenuIds = convertSet(roleMenuMapper.selectListByRoleId(roleId), RoleMenuDO::getMenuId);
Set<Long> menuIdList = CollUtil.emptyIfNull(menuIds);
Collection<Long> createMenuIds = CollUtil.subtract(menuIdList, dbMenuIds);
Collection<Long> deleteMenuIds = CollUtil.subtract(dbMenuIds, menuIdList);
// 执行数据库操作
if (CollUtil.isNotEmpty(createMenuIds)) {
roleMenuMapper.insertBatch(createMenuIds.stream()
.map(menuId -> new RoleMenuDO(roleId, menuId))
.collect(Collectors.toList()));
}
if (CollUtil.isNotEmpty(deleteMenuIds)) {
roleMenuMapper.deleteListByRoleIdAndMenuIds(roleId, deleteMenuIds);
}
}
2. 数据权限控制
ruoyi-vue-pro支持多种数据权限范围,通过DataScopeEnum定义:
| 数据范围 | 值 | 描述 |
|---|---|---|
| ALL | 1 | 所有数据权限 |
| DEPT_CUSTOM | 2 | 自定义部门数据权限 |
| DEPT_ONLY | 3 | 本部门数据权限 |
| DEPT_AND_CHILD | 4 | 本部门及以下数据权限 |
| SELF | 5 | 仅本人数据权限 |
// 数据权限获取实现
@Override
@DataPermission(enable = false)
public DeptDataPermissionRespDTO getDeptDataPermission(Long userId) {
List<RoleDO> roles = getEnableUserRoleListByUserIdFromCache(userId);
DeptDataPermissionRespDTO result = new DeptDataPermissionRespDTO();
if (CollUtil.isEmpty(roles)) {
result.setSelf(true);
return result;
}
// 遍历角色计算数据权限
for (RoleDO role : roles) {
switch (DataScopeEnum.getByScope(role.getDataScope())) {
case ALL:
result.setAll(true);
break;
case DEPT_CUSTOM:
result.getDeptIds().addAll(role.getDataScopeDeptIds());
break;
case DEPT_ONLY:
result.getDeptIds().add(userDeptId.get());
break;
case DEPT_AND_CHILD:
result.getDeptIds().addAll(deptService.getChildDeptIdListFromCache(userDeptId.get()));
break;
case SELF:
result.setSelf(true);
break;
}
}
return result;
}
🚀 前端动态路由实现
1. 路由初始化流程
前端动态路由的生成遵循以下流程:
2. 权限指令实现
前端通过自定义指令v-permission实现按钮级别的权限控制:
// 权限指令实现
const permission = {
mounted(el, binding) {
const { value } = binding
const roles = store.getters.roles
if (value && value instanceof Array && value.length > 0) {
const permissionRoles = value
const hasPermission = roles.some(role => {
return permissionRoles.includes(role)
})
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error(`需要权限角色! Like v-permission="['admin','editor']"`)
}
}
}
// 全局注册
app.directive('permission', permission)
3. 路由守卫配置
路由守卫确保用户在访问页面时具备相应权限:
// 路由守卫实现
router.beforeEach(async (to, from, next) => {
// 设置页面title
document.title = getPageTitle(to.meta.title)
// 确定用户是否已登录
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
next({ path: '/' })
} else {
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
try {
// 获取用户信息
const { roles } = await store.dispatch('user/getInfo')
// 根据角色生成可访问的路由
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// 动态添加路由
accessRoutes.forEach(route => {
router.addRoute(route)
})
// 确保addRoutes已完成
next({ ...to, replace: true })
} catch (error) {
// 移除token并跳转到登录页
await store.dispatch('user/resetToken')
next(`/login?redirect=${to.path}`)
}
}
}
} else {
// 未登录的处理逻辑
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
next(`/login?redirect=${to.path}`)
}
}
})
📊 缓存优化策略
ruoyi-vue-pro采用Redis缓存优化权限查询性能:
| 缓存Key | 描述 | 过期策略 |
|---|---|---|
PERMISSION_MENU_ID_LIST:{permission} | 权限对应的菜单ID列表 | 菜单变更时清除 |
MENU_ROLE_ID_LIST:{menuId} | 菜单对应的角色ID列表 | 角色权限变更时清除 |
USER_ROLE_ID_LIST:{userId} | 用户对应的角色ID列表 | 用户角色变更时清除 |
// 缓存配置示例
@Cacheable(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId")
public Set<Long> getUserRoleIdListByUserIdFromCache(Long userId) {
return getUserRoleIdListByUserId(userId);
}
@Cacheable(value = RedisKeyConstants.MENU_ROLE_ID_LIST, key = "#menuId")
public Set<Long> getMenuRoleIdListByMenuIdFromCache(Long menuId) {
return convertSet(roleMenuMapper.selectListByMenuId(menuId), RoleMenuDO::getRoleId);
}
🔧 最佳实践指南
1. 权限配置流程
2. 权限标识规范
建议遵循统一的权限标识命名规范:
- 模块:操作:资源
- 例如:
system:user:create、system:menu:query
3. 数据权限配置
数据权限配置表结构:
| 字段名 | 类型 | 描述 |
|---|---|---|
| role_id | bigint | 角色ID |
| data_scope | int | 数据范围类型 |
| data_scope_dept_ids | varchar | 自定义部门ID列表 |
🎯 性能优化建议
- 缓存策略:合理使用Redis缓存权限数据,减少数据库查询
- 懒加载:路由组件按需加载,提升首屏加载速度
- 批量操作:权限分配时使用批量操作,减少数据库IO
- 索引优化:为权限相关表建立合适的索引
📝 总结
ruoyi-vue-pro的动态路由和权限控制体系提供了完整的企业级解决方案:
- 精细化控制:支持菜单、按钮、数据多维度权限控制
- 高性能:通过Redis缓存和优化查询提升系统性能
- 易扩展:模块化设计便于功能扩展和定制
- 安全可靠:完善的权限校验机制保障系统安全
通过本文的深入解析,开发者可以更好地理解ruoyi-vue-pro的权限控制机制,并在实际项目中灵活运用,构建安全可靠的后台管理系统。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



