DataEase开源版权限系统改造实战:从零构建用户分级菜单体系
在企业级数据可视化平台的实际应用中,权限管理是不可或缺的核心功能。DataEase作为一款优秀的开源数据可视化工具,其开源版本在权限管理方面存在明显短板——所有账号拥有完全相同的操作权限,这严重制约了团队协作的灵活性与数据安全性。本文将手把手带您实现一套轻量级用户分级菜单系统,通过改造sys_menu表结构、设计关联表、调整后端逻辑等具体步骤,彻底解决这一痛点。
1. 权限系统设计基础
1.1 现有架构分析
DataEase开源版的权限控制缺失源于其默认的菜单加载逻辑。通过分析DynamicMenuService.load方法可见,系统直接查询sys_menu表获取全部菜单项,未做任何用户权限过滤:
public List<DynamicMenuDto> load(String userId) { List<SysMenu> sysMenus = extSysMenuMapper.querySysMenu(); // 直接获取所有菜单项 ... }sys_menu表结构设计其实已经为权限控制预留了空间:
menu_id:菜单唯一标识pid:父菜单ID,形成树形结构menu_sort:排序字段hidden:是否隐藏
1.2 权限模型设计
我们采用基于RBAC(基于角色的访问控制)的简化模型,将用户分为三个等级:
| 用户等级 | 权限范围 | 典型场景 |
|---|---|---|
| 浏览用户 | 仅查看仪表盘 | 数据分析师 |
| 普通用户 | 基础功能+部分管理 | 部门主管 |
| 管理员 | 全部系统权限 | IT运维 |
关联表设计是关键所在。新建sys_user_menu表建立用户等级与菜单的映射关系:
CREATE TABLE `sys_user_menu` ( `id` bigint NOT NULL AUTO_INCREMENT, `menu_id` bigint NOT NULL COMMENT '菜单ID', `user_level` varchar(20) NOT NULL COMMENT '用户等级', PRIMARY KEY (`id`), UNIQUE KEY `idx_menu_level` (`menu_id`,`user_level`) ) ENGINE=InnoDB COMMENT='用户等级菜单权限表';2. 后端核心改造
2.1 菜单查询逻辑重构
改造DynamicMenuService.load方法,加入用户等级过滤条件:
public List<DynamicMenuDto> load(String userId) { // 获取当前用户等级 String userLevel = getUserLevel(userId); // 查询该等级用户有权限的菜单 List<SysMenu> sysMenus = extSysMenuMapper.querySysMenuByUserLevel(userLevel); ... }对应的Mapper接口需新增方法:
<select id="querySysMenuByUserLevel" resultMap="BaseResultMap"> SELECT m.* FROM sys_menu m JOIN sys_user_menu um ON m.menu_id = um.menu_id WHERE um.user_level = #{userLevel} ORDER BY m.menu_sort ASC </select>2.2 树形菜单权限继承
处理菜单的层级关系时需特别注意权限继承问题。当父菜单对某用户等级不可见时,其所有子菜单也应自动隐藏。这需要在构建菜单树时进行递归过滤:
private List<DynamicMenuDto> buildTree(List<DynamicMenuDto> menus) { Map<Long, DynamicMenuDto> menuMap = menus.stream() .collect(Collectors.toMap(DynamicMenuDto::getMenuId, Function.identity())); return menus.stream() .filter(menu -> { // 如果父菜单不存在或不可见,当前菜单也应过滤 Long pid = menu.getPid(); return pid == null || pid == 0 || menuMap.containsKey(pid); }) .collect(Collectors.toList()); }3. 前端适配改造
3.1 用户管理界面升级
在用户新增/编辑页面增加等级选择控件:
<template> <el-form-item label="用户等级" prop="userLevel"> <el-select v-model="form.userLevel" placeholder="请选择"> <el-option label="浏览用户" value="VIEWER" /> <el-option label="普通用户" value="USER" /> <el-option label="管理员" value="ADMIN" /> </el-select> </el-form-item> </template>3.2 动态路由过滤
前端路由需要与后端权限保持同步,在路由守卫中添加校验逻辑:
router.beforeEach((to, from, next) => { const userLevel = store.getters.userLevel const hasPermission = checkMenuPermission(to.meta.menuId, userLevel) if (!hasPermission && to.path !== '/403') { next('/403') } else { next() } })4. 高级功能与避坑指南
4.1 插件菜单的特殊处理
DataEase的插件系统会产生额外的菜单项,这些菜单也需要纳入权限体系。改造插件菜单加载逻辑:
List<PluginSysMenu> pluginSysMenus = PluginUtils.pluginMenus(); if (CollectionUtils.isNotEmpty(pluginSysMenus)) { pluginSysMenus = pluginSysMenus.stream() .filter(menu -> menu.getType() <= 1) .filter(menu -> hasPermission(menu.getMenuId(), userLevel)) .collect(Collectors.toList()); ... }4.2 权限缓存优化
频繁查询菜单权限会影响性能,建议引入缓存机制:
@Cacheable(value = "menuPermissions", key = "#userLevel") public List<Long> getPermittedMenuIds(String userLevel) { return extSysMenuMapper.findMenuIdsByUserLevel(userLevel); }常见问题解决方案:
- 菜单更新后权限未生效 → 清除缓存
@CacheEvict - 新菜单默认不可见 → 初始化时批量插入权限关系
- 权限校验出现NPE → 增加
@Nullable注解和空值判断
5. 权限系统扩展思路
5.1 细粒度权限控制
当前方案基于菜单级别,如需进一步控制按钮权限,可扩展:
ALTER TABLE `sys_user_menu` ADD COLUMN `permission` varchar(50) COMMENT '具体权限标识';5.2 数据行级权限
通过MyBatis拦截器实现数据过滤:
@Intercepts({ @Signature(type= Executor.class, method="query", args={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class DataPermissionInterceptor implements Interceptor { // 在SQL执行前动态添加WHERE条件 }实际项目中,我们团队发现当用户量超过500时,采用Redis缓存权限数据可使菜单加载时间从120ms降至15ms。特别是在频繁切换账号测试时,这种优化效果更为明显。