Closed liyujiang-gzu closed 2 days ago
碰到问题,请在 https://github.com/YunaiV/ruoyi-vue-pro/issues 搜索是否存在相似的 issue。
不按照模板提交的 issue,会被系统自动删除。
(必填)我花费了 2-4 小时自查,发现可能的原因是:xxxxxx
第一步,
第二步,
第三步,
数据权限业务组件应该可以优化下,改成更通用的,取消部门数据权限,核心的重构代码如下:
/** * 数据权限规则接口 * 通过实现接口,自定义数据规则。例如说, * * @author 芋道源码 */ public interface DataPermissionRule { /** * 返回需要生效的表名数组 * 为什么需要该方法?Data Permission 数组基于 SQL 重写,通过 Where 返回只有权限的数据 * <p> * 如果需要基于实体名获得表名,可调用 {@link TableInfoHelper#getTableInfo(Class)} 获得 * * @return 表名数组 */ Set<String> getTableNames(); /** * 根据表名和别名,生成对应的 WHERE / OR 过滤条件 * * @param tableName 表名 * @param tableAlias 别名,可能为空 * @return 过滤条件 Expression 表达式 */ Expression getExpression(String tableName, Alias tableAlias); /** * 添加数据权限过滤的数据编号列 * * @param entityClass 实体类 * @param columnName 列名 */ void addDataColumn(Class<? extends BaseDO> entityClass, String columnName); /** * 添加数据权限过滤的数据编号列 * * @param tableName 表名 * @param columnName 列名 */ void addDataColumn(String tableName, String columnName); /** * 添加数据权限过滤的用户编号列 * * @param entityClass 实体类 * @param columnName 列名 */ void addUserColumn(Class<? extends BaseDO> entityClass, String columnName); /** * 添加数据权限过滤的用户编号列 * * @param tableName 表名 * @param columnName 列名 */ void addUserColumn(String tableName, String columnName); }
/** * {@link DataPermissionRuleImpl} 的自定义配置接口 * * @author 芋道源码 */ @FunctionalInterface public interface DataPermissionRuleCustomizer { /** * 自定义该权限规则 * 1. 调用 {@link DataPermissionRuleImpl#addDataColumn(Class, String)} 方法,配置基于数据编号的过滤规则 * 2. 调用 {@link DataPermissionRuleImpl#addUserColumn(Class, String)} 方法,配置基于用户编号的过滤规则 * * @param rule 权限规则 */ void customize(DataPermissionRule rule); }
/** * 默认的 {@link DataPermissionRule} 实现类 * <p> * 注意,使用 {@link DataPermissionRuleImpl} 时,需要保证表中有数据编号的字段,可自定义。 * <p> * 实际业务场景下,会存在一个经典的问题?当用户修改数据时,冗余的数据编号是否需要修改? * 1. 一般情况下,数据编号不进行修改,则会导致用户看不到之前的数据。【yudao-server 采用该方案】 * 2. 部分情况下,希望该用户还是能看到之前的数据,则有两种方式解决:【需要你改造该 {@link DataPermissionRuleImpl} 的实现代码】 * 1)编写洗数据的脚本,将旧的数据编号修改成新数据的编号;【建议】 * 最终过滤条件是`WHERE 数据编号 = ?` * 2)洗数据的话,可能涉及的数据量较大,也可以采用用户编号进行过滤的方式,此时需要获取到数据编号对应的所有用户编号; * 最终过滤条件是`WHERE 用户编号 IN (?, ?, ? ...)` * 3)想要保证原数据编号和用户编号都可以看的到,此时使用数据编号和用户编号一起过滤; * 最终过滤条件是`WHERE 数据编号 = ? OR 用户编号 IN (?, ?, ? ...)` * * @author 芋道源码 */ @Slf4j public class DataPermissionRuleImpl implements DataPermissionRule { /** * LoginUser 的 Context 缓存 Key */ public static final String CONTEXT_KEY = DataPermissionRuleImpl.class.getSimpleName(); /** * WHERE 语句中添加 AND NULL */ public static final Expression EXPRESSION_NULL = new NullValue(); /** * 基于数据的表字段配置 TODO 目前只支持一个数据权限字段,dept_id 和 user_id 不能同时配置。 * 如基于部门的数据编号字段是 dept_id,基于店铺的数据编号字段是 shop_id,通过该配置自定义,如 group_id。 * <p> * key:表名 * value:字段名 */ private final static Map<String, String> DATA_COLUMNS = new HashMap<>(); /** * 基于用户的表字段配置 * 一般情况下,每个表的数据编号字段是 user_id,通过该配置自定义,如 admin_user_id。 * <p> * key:表名 * value:字段名 */ private final static Map<String, String> USER_COLUMNS = new HashMap<>(); /** * 所有表名,是 {@link #DATA_COLUMNS} 和 {@link #USER_COLUMNS} 的合集 */ private final static Set<String> TABLE_NAMES = new HashSet<>(); private final PermissionApi permissionApi; public DataPermissionRuleImpl(PermissionApi permissionApi) { this.permissionApi = permissionApi; } @Override public Set<String> getTableNames() { return TABLE_NAMES; } @Override public Expression getExpression(String tableName, Alias tableAlias) { // 只有有登录用户的情况下,才进行数据权限的处理 LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); if (loginUser == null) { return null; } // 只有管理员类型的用户,才进行自定义数据权限的处理,普通用户直接通过 user_id 进行数据权限过滤即可 if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) { return null; } // 获得数据权限 DataPermissionRespDTO dataPermissionRespDTO = loginUser.getContextKeyValue(CONTEXT_KEY, DataPermissionRespDTO.class); // 从上下文中拿不到,则调用逻辑进行获取 if (dataPermissionRespDTO == null) { dataPermissionRespDTO = permissionApi.getDataPermission(loginUser.getId()); if (dataPermissionRespDTO == null) { log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser)); throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限", loginUser.getId(), tableName, tableAlias.getName())); } // 添加到上下文中,避免重复计算 loginUser.putContextKeyValue(CONTEXT_KEY, dataPermissionRespDTO); } // 情况一,如果是 ALL 可查看全部,则无需拼接条件 if (dataPermissionRespDTO.getAll()) { return null; } // 情况二,即不能查看数据,又不能查看自己,则说明 100% 无权限 if (CollUtil.isEmpty(dataPermissionRespDTO.getDataIds()) && Boolean.FALSE.equals(dataPermissionRespDTO.getSelf())) { return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空 } // 情况三,拼接 Shop 和 User 的条件,最后组合 Expression dataExpression = buildDataExpression(tableName, tableAlias, dataPermissionRespDTO.getDataIds()); Expression userExpression = buildUserExpression(tableName, tableAlias, dataPermissionRespDTO.getSelf(), loginUser.getId()); if (dataExpression == null && userExpression == null) { // 芋艿:获得不到条件的时候,暂时不抛出异常,而是不返回数据 log.warn("[getExpression][Table({}/{}) LoginUser({}) ShopDataPermission({}) 构建的条件为空]", tableName, tableAlias, JsonUtils.toJsonString(loginUser), JsonUtils.toJsonString(dataPermissionRespDTO)); return EXPRESSION_NULL; // AND NULL } if (dataExpression == null) { return userExpression; } if (userExpression == null) { return dataExpression; } // 目前,如果有指定数据 + 可查看自己,采用 OR 条件。即,WHERE (shop_id IN ? OR user_id = ?) return new Parenthesis(new OrExpression(dataExpression, userExpression)); } @Override public void addDataColumn(Class<? extends BaseDO> entityClass, String columnName) { addDataColumn(getTableName(entityClass), columnName); } @Override public void addDataColumn(String tableName, String columnName) { if (CharSequenceUtil.isEmpty(tableName)) { return; } DATA_COLUMNS.put(tableName, columnName); TABLE_NAMES.add(tableName); } @Override public void addUserColumn(Class<? extends BaseDO> entityClass, String columnName) { addDataColumn(getTableName(entityClass), columnName); } @Override public void addUserColumn(String tableName, String columnName) { if (CharSequenceUtil.isEmpty(tableName)) { return; } USER_COLUMNS.put(tableName, columnName); TABLE_NAMES.add(tableName); } private Expression buildDataExpression(String tableName, Alias tableAlias, Map<String, Set<Long>> dataIdMap) { // 如果不存在配置,则无需作为条件 String columnName = DATA_COLUMNS.get(tableName); if (StrUtil.isEmpty(columnName) || CollUtil.isEmpty(dataIdMap)) { return null; } HashSet<Long> emptySet = CollUtil.newHashSet(); Set<Long> dataIds = dataIdMap.getOrDefault(columnName, emptySet); if (CollUtil.isEmpty(dataIds)) { return null; } // 拼接条件 if (dataIds.size() == 1) { return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(CollUtil.newArrayList(dataIds).getFirst())); } return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new ExpressionList(CollectionUtils.convertList(dataIds, LongValue::new))); } private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) { // 如果不查看自己,则无需作为条件 String columnName = USER_COLUMNS.get(tableName); if (Boolean.FALSE.equals(self) || StrUtil.isEmpty(columnName)) { return null; } // 拼接条件 return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId)); } private String getTableName(Class<? extends BaseDO> entityClass) { TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass); if (tableInfo == null) { log.warn("从实体类({})获取不到数据库表信息", entityClass.getSimpleName()); return null; } return tableInfo.getTableName(); } }
/** * 数据权限 Response DTO * * @author 芋道源码 */ @Data public class DataPermissionRespDTO { /** * 是否可查看全部数据 */ private Boolean all; /** * 是否可查看自己的数据 */ private Boolean self; /** * 可查看的数据字段及数据编号数组键值对 */ private Map<String, Set<Long>> dataIds; public DataPermissionRespDTO() { this.all = false; this.self = false; this.dataIds = new HashMap<>(); } }
@Override @DataPermission(enable = false) // 关闭数据权限,不然就会出现递归获取数据权限的问题 public DataPermissionRespDTO getDataPermission(Long userId) { DataPermissionRespDTO result = new DataPermissionRespDTO(); result.setAll(false); result.setSelf(false); result.setDataIds(new HashMap<>()); AdminUserDO user = userService.getUserFromCache(userId); // 用户信息不存在,则没有任何数据权限 if (user == null) { return result; } // 默认有自己的数据权限 result.setSelf(true); // 获得用户的角色 List<RoleDO> roles = getEnableUserRoleListByUserIdFromCache(userId); // 如果角色为空,则只能查看自己 if (CollUtil.isEmpty(roles)) { return result; } // 遍历每个角色,计算 for (RoleDO role : roles) { // 角色为空时,跳过 if (role.getDataScope() == null) { continue; } // 所有的数据权限 if (Objects.equals(role.getDataScope(), DataScopeEnum.ALL.getScope())) { result.setAll(true); continue; } // TODO 数据编号(dept_id、shop_id)这里暂时写死,为了解耦,需要优化 // 自己可管理的店铺数据 Set<Long> userShopIds = shopAdminApi.getShopIdsByUserIdFromCache(userId); result.getDataIds().put(PermissionApi.DATA_ID_SHOP, userShopIds); // 自己所在的部门数据权限 if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_ONLY.getScope())) { Set<Long> roleDeptIds = new HashSet<>(); CollUtil.addAll(roleDeptIds, user.getDeptId()); result.getDataIds().put(PermissionApi.DATA_ID_DEPT, roleDeptIds); continue; } // 自己所在的部门及指定部门数据权限 if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_CUSTOM.getScope())) { Set<Long> roleDeptIds = new HashSet<>(); CollUtil.addAll(roleDeptIds, user.getDeptId()); CollUtil.addAll(roleDeptIds, role.getDataScopeDeptIds()); result.getDataIds().put(PermissionApi.DATA_ID_DEPT, roleDeptIds); continue; } // 自己所在的部门及下级部门数据权限 if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_AND_CHILD.getScope())) { Set<Long> roleDeptIds = new HashSet<>(); CollUtil.addAll(roleDeptIds, user.getDeptId()); CollUtil.addAll(roleDeptIds, deptService.getChildDeptIdListFromCache(user.getDeptId())); result.getDataIds().put(PermissionApi.DATA_ID_DEPT, roleDeptIds); continue; } // 未知情况,记录错误日志 log.error("[getDataPermission][LoginUser({}) role({}) 无法处理]", userId, toJsonString(result)); } return result; }
取消内置部门数据权限的理由:
一些业务系统不得部门的需求; 部门编号和角色、会员耦合太严重,不通用。 用户相关的部门+用户相关的角色=>角色相关的部门,也许适合移到具体的业务模块内,部门编号都不和系统模块及会员模块耦合,可在在具体的业务模块内增加关联表来;
如果这个情况,可能我更倾向上,我其实更倾向,在写一个 Rule 哈
感谢贡献哈。
我先写到文档里,
碰到问题,请在 https://github.com/YunaiV/ruoyi-vue-pro/issues 搜索是否存在相似的 issue。
不按照模板提交的 issue,会被系统自动删除。
基本信息
你猜测可能的原因
(必填)我花费了 2-4 小时自查,发现可能的原因是:xxxxxx
复现步骤
第一步,
第二步,
第三步,
报错信息
数据权限业务组件应该可以优化下,改成更通用的,取消部门数据权限,核心的重构代码如下: