From 5794780c4e11da7868d90a9f4c635889ea6a906f Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 13 May 2022 00:24:10 +0800 Subject: [PATCH] 调整 SecurityPermissionFrameworkService 的实现,完善 PermissionServiceTest 的单元测试 --- yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml | 7 +++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java | 12 ++++++------ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/package-info.java | 6 ------ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRule.java | 205 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleCustomizer.java | 20 -------------------- yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/DeptDataPermissionFrameworkService.java | 21 --------------------- yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/dto/DeptDataPermissionRespDTO.java | 35 ----------------------------------- yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java | 208 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java | 20 ++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/package-info.java | 6 ++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleTest.java | 235 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java | 235 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java | 8 ++++++++ yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkService.java | 44 ++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java | 38 ++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityPermissionFrameworkService.java | 44 -------------------------------------------- yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApi.java | 28 ++++++++++++++++++++++++++++ yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/dto/DeptDataPermissionRespDTO.java | 35 +++++++++++++++++++++++++++++++++++ yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApiImpl.java | 16 ++++++++++++++++ yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/datapermission/config/DataPermissionConfiguration.java | 2 +- yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionService.java | 30 +++++++++++++++++++++++++++--- yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java | 26 ++++++++------------------ yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AuthServiceImplTest.java | 3 +++ yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------- 24 files changed, 769 insertions(+), 611 deletions(-) delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/package-info.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRule.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleCustomizer.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/DeptDataPermissionFrameworkService.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/dto/DeptDataPermissionRespDTO.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/package-info.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleTest.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java create mode 100644 yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkService.java create mode 100644 yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityPermissionFrameworkService.java create mode 100644 yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/dto/DeptDataPermissionRespDTO.java diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml index 802ff19..09e8786 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml @@ -34,6 +34,13 @@ yudao-spring-boot-starter-mybatis + + + cn.iocoder.boot + yudao-module-system-api + ${revision} + + cn.iocoder.boot diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java index b66c176..92da456 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java @@ -1,9 +1,9 @@ package cn.iocoder.yudao.framework.datapermission.config; -import cn.iocoder.yudao.framework.datapermission.core.dept.rule.DeptDataPermissionRule; -import cn.iocoder.yudao.framework.datapermission.core.dept.rule.DeptDataPermissionRuleCustomizer; -import cn.iocoder.yudao.framework.datapermission.core.dept.service.DeptDataPermissionFrameworkService; +import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRule; +import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer; import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.context.annotation.Bean; @@ -18,14 +18,14 @@ import java.util.List; */ @Configuration @ConditionalOnClass(LoginUser.class) -@ConditionalOnBean(value = {DeptDataPermissionFrameworkService.class, DeptDataPermissionRuleCustomizer.class}) +@ConditionalOnBean(value = {PermissionApi.class, DeptDataPermissionRuleCustomizer.class}) public class YudaoDeptDataPermissionAutoConfiguration { @Bean - public DeptDataPermissionRule deptDataPermissionRule(DeptDataPermissionFrameworkService service, + public DeptDataPermissionRule deptDataPermissionRule(PermissionApi permissionApi, List customizers) { // 创建 DeptDataPermissionRule 对象 - DeptDataPermissionRule rule = new DeptDataPermissionRule(service); + DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi); // 补全表配置 customizers.forEach(customizer -> customizer.customize(rule)); return rule; diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/package-info.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/package-info.java deleted file mode 100644 index 20daa85..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * 基于部门的数据权限规则 - * - * @author 芋道源码 - */ -package cn.iocoder.yudao.framework.datapermission.core.dept; diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRule.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRule.java deleted file mode 100644 index 2a0c9ae..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRule.java +++ /dev/null @@ -1,205 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.dept.rule; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.ObjectUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.framework.datapermission.core.dept.service.DeptDataPermissionFrameworkService; -import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO; -import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; -import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; -import cn.iocoder.yudao.framework.security.core.LoginUser; -import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; -import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import net.sf.jsqlparser.expression.Alias; -import net.sf.jsqlparser.expression.Expression; -import net.sf.jsqlparser.expression.LongValue; -import net.sf.jsqlparser.expression.NullValue; -import net.sf.jsqlparser.expression.operators.conditional.OrExpression; -import net.sf.jsqlparser.expression.operators.relational.EqualsTo; -import net.sf.jsqlparser.expression.operators.relational.ExpressionList; -import net.sf.jsqlparser.expression.operators.relational.InExpression; - -import java.util.*; - -/** - * 基于部门的 {@link DataPermissionRule} 数据权限规则实现 - * - * 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。 - * - * 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改? - * 1. 一般情况下,dept_id 不进行修改,则会导致用户看到之前的数据。【yudao-server 采用该方案】 - * 2. 部分情况下,希望该用户还是能看到之前的数据,则有两种方式解决:【需要你改造该 DeptDataPermissionRule 的实现代码】 - * 1)编写洗数据的脚本,将 dept_id 修改成新部门的编号;【建议】 - * 最终过滤条件是 WHERE dept_id = ? - * 2)洗数据的话,可能涉及的数据量较大,也可以采用 user_id 进行过滤的方式,此时需要获取到 dept_id 对应的所有 user_id 用户编号; - * 最终过滤条件是 WHERE user_id IN (?, ?, ? ...) - * 3)想要保证原 dept_id 和 user_id 都可以看的到,此时使用 dept_id 和 user_id 一起过滤; - * 最终过滤条件是 WHERE dept_id = ? OR user_id IN (?, ?, ? ...) - * - * @author 芋道源码 - */ -@AllArgsConstructor -@Slf4j -public class DeptDataPermissionRule implements DataPermissionRule { - - /** - * LoginUser 的 Context 缓存 Key - */ - protected static final String CONTEXT_KEY = DeptDataPermissionRule.class.getSimpleName(); - - private static final String DEPT_COLUMN_NAME = "dept_id"; - private static final String USER_COLUMN_NAME = "user_id"; - - static final Expression EXPRESSION_NULL = new NullValue(); - - private final DeptDataPermissionFrameworkService deptDataPermissionService; - - /** - * 基于部门的表字段配置 - * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 - * - * key:表名 - * value:字段名 - */ - private final Map deptColumns = new HashMap<>(); - /** - * 基于用户的表字段配置 - * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 - * - * key:表名 - * value:字段名 - */ - private final Map userColumns = new HashMap<>(); - /** - * 所有表名,是 {@link #deptColumns} 和 {@link #userColumns} 的合集 - */ - private final Set TABLE_NAMES = new HashSet<>(); - - @Override - public Set getTableNames() { - return TABLE_NAMES; - } - - @Override - public Expression getExpression(String tableName, Alias tableAlias) { - // 只有有登陆用户的情况下,才进行数据权限的处理 - LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); - if (loginUser == null) { - return null; - } - // 只有管理员类型的用户,才进行数据权限的处理 - if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) { - return null; - } - - // 获得数据权限 - DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class); - // 从上下文中拿不到,则调用逻辑进行获取 - if (deptDataPermission == null) { - deptDataPermission = deptDataPermissionService.getDeptDataPermission(loginUser.getId()); - if (deptDataPermission == 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.setContext(CONTEXT_KEY, deptDataPermission); - } - - // 情况一,如果是 ALL 可查看全部,则无需拼接条件 - if (deptDataPermission.getAll()) { - return null; - } - - // 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限 - if (CollUtil.isEmpty(deptDataPermission.getDeptIds()) - && Boolean.FALSE.equals(deptDataPermission.getSelf())) { - return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空 - } - - // 情况三,拼接 Dept 和 User 的条件,最后组合 - Expression deptExpression = buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds()); - Expression userExpression = buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId()); - if (deptExpression == null && userExpression == null) { - // TODO 芋艿:获得不到条件的时候,暂时不抛出异常,而是不返回数据 - log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]", - JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission)); -// throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空", -// loginUser.getId(), tableName, tableAlias.getName())); - return EXPRESSION_NULL; - } - if (deptExpression == null) { - return userExpression; - } - if (userExpression == null) { - return deptExpression; - } - // 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE dept_id IN ? OR user_id = ? - return new OrExpression(deptExpression, userExpression); - } - - private Expression buildDeptExpression(String tableName, Alias tableAlias, Set deptIds) { - // 如果不存在配置,则无需作为条件 - String columnName = deptColumns.get(tableName); - if (StrUtil.isEmpty(columnName)) { - return null; - } - // 如果为空,则无条件 - if (CollUtil.isEmpty(deptIds)) { - return null; - } - // 拼接条件 - return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), - new ExpressionList(CollectionUtils.convertList(deptIds, LongValue::new))); - } - - private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) { - // 如果不查看自己,则无需作为条件 - if (Boolean.FALSE.equals(self)) { - return null; - } - String columnName = userColumns.get(tableName); - if (StrUtil.isEmpty(columnName)) { - return null; - } - // 拼接条件 - return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId)); - } - - // ==================== 添加配置 ==================== - - public void addDeptColumn(Class entityClass) { - addDeptColumn(entityClass, DEPT_COLUMN_NAME); - } - - public void addDeptColumn(Class entityClass, String columnName) { - String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); - addDeptColumn(tableName, columnName); - } - - public void addDeptColumn(String tableName, String columnName) { - deptColumns.put(tableName, columnName); - TABLE_NAMES.add(tableName); - } - - public void addUserColumn(Class entityClass) { - addUserColumn(entityClass, USER_COLUMN_NAME); - } - - public void addUserColumn(Class entityClass, String columnName) { - String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); - addUserColumn(tableName, columnName); - } - - public void addUserColumn(String tableName, String columnName) { - userColumns.put(tableName, columnName); - TABLE_NAMES.add(tableName); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleCustomizer.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleCustomizer.java deleted file mode 100644 index 5341ee5..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleCustomizer.java +++ /dev/null @@ -1,20 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.dept.rule; - -/** - * {@link DeptDataPermissionRule} 的自定义配置接口 - * - * @author 芋道源码 - */ -@FunctionalInterface -public interface DeptDataPermissionRuleCustomizer { - - /** - * 自定义该权限规则 - * 1. 调用 {@link DeptDataPermissionRule#addDeptColumn(Class, String)} 方法,配置基于 dept_id 的过滤规则 - * 2. 调用 {@link DeptDataPermissionRule#addUserColumn(Class, String)} 方法,配置基于 user_id 的过滤规则 - * - * @param rule 权限规则 - */ - void customize(DeptDataPermissionRule rule); - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/DeptDataPermissionFrameworkService.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/DeptDataPermissionFrameworkService.java deleted file mode 100644 index e491aa0..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/DeptDataPermissionFrameworkService.java +++ /dev/null @@ -1,21 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.dept.service; - -import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO; - -/** - * 基于部门的数据权限 Framework Service 接口 - * 目前的实现类是 SysPermissionServiceImpl 类 - * - * @author 芋道源码 - */ -public interface DeptDataPermissionFrameworkService { - - /** - * 获得登陆用户的部门数据权限 - * - * @param userId 用户编号 - * @return 部门数据权限 - */ - DeptDataPermissionRespDTO getDeptDataPermission(Long userId); - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/dto/DeptDataPermissionRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/dto/DeptDataPermissionRespDTO.java deleted file mode 100644 index 897fb22..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/dto/DeptDataPermissionRespDTO.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.dept.service.dto; - -import lombok.Data; - -import java.util.HashSet; -import java.util.Set; - -/** - * 部门的数据权限 Response DTO - * - * @author 芋道源码 - */ -@Data -public class DeptDataPermissionRespDTO { - - /** - * 是否可查看全部数据 - */ - private Boolean all; - /** - * 是否可查看自己的数据 - */ - private Boolean self; - /** - * 可查看的部门编号数组 - */ - private Set deptIds; - - public DeptDataPermissionRespDTO() { - this.all = false; - this.self = false; - this.deptIds = new HashSet<>(); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java new file mode 100644 index 0000000..7f9a74a --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java @@ -0,0 +1,208 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule.dept; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.NullValue; +import net.sf.jsqlparser.expression.operators.conditional.OrExpression; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.expression.operators.relational.InExpression; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * 基于部门的 {@link DataPermissionRule} 数据权限规则实现 + * + * 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。 + * + * 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改? + * 1. 一般情况下,dept_id 不进行修改,则会导致用户看到之前的数据。【yudao-server 采用该方案】 + * 2. 部分情况下,希望该用户还是能看到之前的数据,则有两种方式解决:【需要你改造该 DeptDataPermissionRule 的实现代码】 + * 1)编写洗数据的脚本,将 dept_id 修改成新部门的编号;【建议】 + * 最终过滤条件是 WHERE dept_id = ? + * 2)洗数据的话,可能涉及的数据量较大,也可以采用 user_id 进行过滤的方式,此时需要获取到 dept_id 对应的所有 user_id 用户编号; + * 最终过滤条件是 WHERE user_id IN (?, ?, ? ...) + * 3)想要保证原 dept_id 和 user_id 都可以看的到,此时使用 dept_id 和 user_id 一起过滤; + * 最终过滤条件是 WHERE dept_id = ? OR user_id IN (?, ?, ? ...) + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Slf4j +public class DeptDataPermissionRule implements DataPermissionRule { + + /** + * LoginUser 的 Context 缓存 Key + */ + protected static final String CONTEXT_KEY = DeptDataPermissionRule.class.getSimpleName(); + + private static final String DEPT_COLUMN_NAME = "dept_id"; + private static final String USER_COLUMN_NAME = "user_id"; + + static final Expression EXPRESSION_NULL = new NullValue(); + + private final PermissionApi permissionApi; + + /** + * 基于部门的表字段配置 + * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 + * + * key:表名 + * value:字段名 + */ + private final Map deptColumns = new HashMap<>(); + /** + * 基于用户的表字段配置 + * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 + * + * key:表名 + * value:字段名 + */ + private final Map userColumns = new HashMap<>(); + /** + * 所有表名,是 {@link #deptColumns} 和 {@link #userColumns} 的合集 + */ + private final Set TABLE_NAMES = new HashSet<>(); + + @Override + public Set getTableNames() { + return TABLE_NAMES; + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + // 只有有登陆用户的情况下,才进行数据权限的处理 + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + if (loginUser == null) { + return null; + } + // 只有管理员类型的用户,才进行数据权限的处理 + if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) { + return null; + } + + // 获得数据权限 + DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class); + // 从上下文中拿不到,则调用逻辑进行获取 + if (deptDataPermission == null) { + deptDataPermission = permissionApi.getDeptDataPermission(loginUser.getId()); + if (deptDataPermission == 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.setContext(CONTEXT_KEY, deptDataPermission); + } + + // 情况一,如果是 ALL 可查看全部,则无需拼接条件 + if (deptDataPermission.getAll()) { + return null; + } + + // 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限 + if (CollUtil.isEmpty(deptDataPermission.getDeptIds()) + && Boolean.FALSE.equals(deptDataPermission.getSelf())) { + return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空 + } + + // 情况三,拼接 Dept 和 User 的条件,最后组合 + Expression deptExpression = buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds()); + Expression userExpression = buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId()); + if (deptExpression == null && userExpression == null) { + // TODO 芋艿:获得不到条件的时候,暂时不抛出异常,而是不返回数据 + log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]", + JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission)); +// throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空", +// loginUser.getId(), tableName, tableAlias.getName())); + return EXPRESSION_NULL; + } + if (deptExpression == null) { + return userExpression; + } + if (userExpression == null) { + return deptExpression; + } + // 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE dept_id IN ? OR user_id = ? + return new OrExpression(deptExpression, userExpression); + } + + private Expression buildDeptExpression(String tableName, Alias tableAlias, Set deptIds) { + // 如果不存在配置,则无需作为条件 + String columnName = deptColumns.get(tableName); + if (StrUtil.isEmpty(columnName)) { + return null; + } + // 如果为空,则无条件 + if (CollUtil.isEmpty(deptIds)) { + return null; + } + // 拼接条件 + return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), + new ExpressionList(CollectionUtils.convertList(deptIds, LongValue::new))); + } + + private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) { + // 如果不查看自己,则无需作为条件 + if (Boolean.FALSE.equals(self)) { + return null; + } + String columnName = userColumns.get(tableName); + if (StrUtil.isEmpty(columnName)) { + return null; + } + // 拼接条件 + return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId)); + } + + // ==================== 添加配置 ==================== + + public void addDeptColumn(Class entityClass) { + addDeptColumn(entityClass, DEPT_COLUMN_NAME); + } + + public void addDeptColumn(Class entityClass, String columnName) { + String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); + addDeptColumn(tableName, columnName); + } + + public void addDeptColumn(String tableName, String columnName) { + deptColumns.put(tableName, columnName); + TABLE_NAMES.add(tableName); + } + + public void addUserColumn(Class entityClass) { + addUserColumn(entityClass, USER_COLUMN_NAME); + } + + public void addUserColumn(Class entityClass, String columnName) { + String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); + addUserColumn(tableName, columnName); + } + + public void addUserColumn(String tableName, String columnName) { + userColumns.put(tableName, columnName); + TABLE_NAMES.add(tableName); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java new file mode 100644 index 0000000..e5e4f57 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule.dept; + +/** + * {@link DeptDataPermissionRule} 的自定义配置接口 + * + * @author 芋道源码 + */ +@FunctionalInterface +public interface DeptDataPermissionRuleCustomizer { + + /** + * 自定义该权限规则 + * 1. 调用 {@link DeptDataPermissionRule#addDeptColumn(Class, String)} 方法,配置基于 dept_id 的过滤规则 + * 2. 调用 {@link DeptDataPermissionRule#addUserColumn(Class, String)} 方法,配置基于 user_id 的过滤规则 + * + * @param rule 权限规则 + */ + void customize(DeptDataPermissionRule rule); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/package-info.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/package-info.java new file mode 100644 index 0000000..66e9326 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/package-info.java @@ -0,0 +1,6 @@ +/** + * 基于部门的数据权限规则 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.framework.datapermission.core.rule.dept; diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleTest.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleTest.java deleted file mode 100644 index ef5ff16..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleTest.java +++ /dev/null @@ -1,235 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.dept.rule; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.ReflectUtil; -import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; -import cn.iocoder.yudao.framework.common.util.collection.SetUtils; -import cn.iocoder.yudao.framework.datapermission.core.dept.service.DeptDataPermissionFrameworkService; -import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO; -import cn.iocoder.yudao.framework.security.core.LoginUser; -import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; -import net.sf.jsqlparser.expression.Alias; -import net.sf.jsqlparser.expression.Expression; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockedStatic; - -import java.util.Map; - -import static cn.iocoder.yudao.framework.datapermission.core.dept.rule.DeptDataPermissionRule.EXPRESSION_NULL; -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.same; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.when; - -/** - * {@link DeptDataPermissionRule} 的单元测试 - * - * @author 芋道源码 - */ -class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { - - @InjectMocks - private DeptDataPermissionRule rule; - - @Mock - private DeptDataPermissionFrameworkService deptDataPermissionFrameworkService; - - @BeforeEach - @SuppressWarnings("unchecked") - public void setUp() { - // 清空 rule - rule.getTableNames().clear(); - ((Map) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); - ((Map) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); - } - - @Test // 无 LoginUser - public void testGetExpression_noLoginUser() { - // 准备参数 - String tableName = randomString(); - Alias tableAlias = new Alias(randomString()); - // mock 方法 - - // 调用 - Expression expression = rule.getExpression(tableName, tableAlias); - // 断言 - assertNull(expression); - } - - @Test // 无数据权限时 - public void testGetExpression_noDeptDataPermission() { - try (MockedStatic securityFrameworkUtilsMock - = mockStatic(SecurityFrameworkUtils.class)) { - // 准备参数 - String tableName = "t_user"; - Alias tableAlias = new Alias("u"); - // mock 方法 - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) - .setUserType(UserTypeEnum.ADMIN.getValue())); - securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); - - // 调用 - NullPointerException exception = assertThrows(NullPointerException.class, - () -> rule.getExpression(tableName, tableAlias)); - // 断言 - assertEquals("LoginUser(1) Table(t_user/u) 未返回数据权限", exception.getMessage()); - } - } - - @Test // 全部数据权限 - public void testGetExpression_allDeptDataPermission() { - try (MockedStatic securityFrameworkUtilsMock - = mockStatic(SecurityFrameworkUtils.class)) { - // 准备参数 - String tableName = "t_user"; - Alias tableAlias = new Alias("u"); - // mock 方法(LoginUser) - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) - .setUserType(UserTypeEnum.ADMIN.getValue())); - securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); - // mock 方法(DeptDataPermissionRespDTO) - DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO().setAll(true); - when(deptDataPermissionFrameworkService.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); - - // 调用 - Expression expression = rule.getExpression(tableName, tableAlias); - // 断言 - assertNull(expression); - assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); - } - } - - @Test // 即不能查看部门,又不能查看自己,则说明 100% 无权限 - public void testGetExpression_noDept_noSelf() { - try (MockedStatic securityFrameworkUtilsMock - = mockStatic(SecurityFrameworkUtils.class)) { - // 准备参数 - String tableName = "t_user"; - Alias tableAlias = new Alias("u"); - // mock 方法(LoginUser) - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) - .setUserType(UserTypeEnum.ADMIN.getValue())); - securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); - // mock 方法(DeptDataPermissionRespDTO) - DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO(); - when(deptDataPermissionFrameworkService.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); - - // 调用 - Expression expression = rule.getExpression(tableName, tableAlias); - // 断言 - assertEquals("null = null", expression.toString()); - assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); - } - } - - @Test // 拼接 Dept 和 User 的条件(字段都不符合) - public void testGetExpression_noDeptColumn_noSelfColumn() { - try (MockedStatic securityFrameworkUtilsMock - = mockStatic(SecurityFrameworkUtils.class)) { - // 准备参数 - String tableName = "t_user"; - Alias tableAlias = new Alias("u"); - // mock 方法(LoginUser) - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) - .setUserType(UserTypeEnum.ADMIN.getValue())); - securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); - // mock 方法(DeptDataPermissionRespDTO) - DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() - .setDeptIds(SetUtils.asSet(10L, 20L)).setSelf(true); - when(deptDataPermissionFrameworkService.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); - - // 调用 - Expression expression = rule.getExpression(tableName, tableAlias); - // 断言 - assertSame(EXPRESSION_NULL, expression); - assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); - } - } - - @Test // 拼接 Dept 和 User 的条件(self 符合) - public void testGetExpression_noDeptColumn_yesSelfColumn() { - try (MockedStatic securityFrameworkUtilsMock - = mockStatic(SecurityFrameworkUtils.class)) { - // 准备参数 - String tableName = "t_user"; - Alias tableAlias = new Alias("u"); - // mock 方法(LoginUser) - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) - .setUserType(UserTypeEnum.ADMIN.getValue())); - securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); - // mock 方法(DeptDataPermissionRespDTO) - DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() - .setSelf(true); - when(deptDataPermissionFrameworkService.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); - // 添加 user 字段配置 - rule.addUserColumn("t_user", "id"); - - // 调用 - Expression expression = rule.getExpression(tableName, tableAlias); - // 断言 - assertEquals("u.id = 1", expression.toString()); - assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); - } - } - - @Test // 拼接 Dept 和 User 的条件(dept 符合) - public void testGetExpression_yesDeptColumn_noSelfColumn() { - try (MockedStatic securityFrameworkUtilsMock - = mockStatic(SecurityFrameworkUtils.class)) { - // 准备参数 - String tableName = "t_user"; - Alias tableAlias = new Alias("u"); - // mock 方法(LoginUser) - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) - .setUserType(UserTypeEnum.ADMIN.getValue())); - securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); - // mock 方法(DeptDataPermissionRespDTO) - DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() - .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)); - when(deptDataPermissionFrameworkService.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); - // 添加 dept 字段配置 - rule.addDeptColumn("t_user", "dept_id"); - - // 调用 - Expression expression = rule.getExpression(tableName, tableAlias); - // 断言 - assertEquals("u.dept_id IN (10, 20)", expression.toString()); - assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); - } - } - - @Test // 拼接 Dept 和 User 的条件(dept + self 符合) - public void testGetExpression_yesDeptColumn_yesSelfColumn() { - try (MockedStatic securityFrameworkUtilsMock - = mockStatic(SecurityFrameworkUtils.class)) { - // 准备参数 - String tableName = "t_user"; - Alias tableAlias = new Alias("u"); - // mock 方法(LoginUser) - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) - .setUserType(UserTypeEnum.ADMIN.getValue())); - securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); - // mock 方法(DeptDataPermissionRespDTO) - DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() - .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)).setSelf(true); - when(deptDataPermissionFrameworkService.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); - // 添加 user 字段配置 - rule.addUserColumn("t_user", "id"); - // 添加 dept 字段配置 - rule.addDeptColumn("t_user", "dept_id"); - - // 调用 - Expression expression = rule.getExpression(tableName, tableAlias); - // 断言 - assertEquals("u.dept_id IN (10, 20) OR u.id = 1", expression.toString()); - assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); - } - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java new file mode 100644 index 0000000..a50e238 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java @@ -0,0 +1,235 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule.dept; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; + +import java.util.Map; + +import static cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRule.EXPRESSION_NULL; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +/** + * {@link DeptDataPermissionRule} 的单元测试 + * + * @author 芋道源码 + */ +class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { + + @InjectMocks + private DeptDataPermissionRule rule; + + @Mock + private PermissionApi permissionApi; + + @BeforeEach + @SuppressWarnings("unchecked") + public void setUp() { + // 清空 rule + rule.getTableNames().clear(); + ((Map) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); + ((Map) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); + } + + @Test // 无 LoginUser + public void testGetExpression_noLoginUser() { + // 准备参数 + String tableName = randomString(); + Alias tableAlias = new Alias(randomString()); + // mock 方法 + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertNull(expression); + } + + @Test // 无数据权限时 + public void testGetExpression_noDeptDataPermission() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法 + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + + // 调用 + NullPointerException exception = assertThrows(NullPointerException.class, + () -> rule.getExpression(tableName, tableAlias)); + // 断言 + assertEquals("LoginUser(1) Table(t_user/u) 未返回数据权限", exception.getMessage()); + } + } + + @Test // 全部数据权限 + public void testGetExpression_allDeptDataPermission() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO().setAll(true); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertNull(expression); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + + @Test // 即不能查看部门,又不能查看自己,则说明 100% 无权限 + public void testGetExpression_noDept_noSelf() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO(); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertEquals("null = null", expression.toString()); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + + @Test // 拼接 Dept 和 User 的条件(字段都不符合) + public void testGetExpression_noDeptColumn_noSelfColumn() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setDeptIds(SetUtils.asSet(10L, 20L)).setSelf(true); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertSame(EXPRESSION_NULL, expression); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + + @Test // 拼接 Dept 和 User 的条件(self 符合) + public void testGetExpression_noDeptColumn_yesSelfColumn() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setSelf(true); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + // 添加 user 字段配置 + rule.addUserColumn("t_user", "id"); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertEquals("u.id = 1", expression.toString()); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + + @Test // 拼接 Dept 和 User 的条件(dept 符合) + public void testGetExpression_yesDeptColumn_noSelfColumn() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + // 添加 dept 字段配置 + rule.addDeptColumn("t_user", "dept_id"); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertEquals("u.dept_id IN (10, 20)", expression.toString()); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + + @Test // 拼接 Dept 和 User 的条件(dept + self 符合) + public void testGetExpression_yesDeptColumn_yesSelfColumn() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)).setSelf(true); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + // 添加 user 字段配置 + rule.addUserColumn("t_user", "id"); + // 添加 dept 字段配置 + rule.addDeptColumn("t_user", "dept_id"); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertEquals("u.dept_id IN (10, 20) OR u.id = 1", expression.toString()); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java index 4933969..c5f2ef0 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java @@ -5,8 +5,11 @@ import cn.iocoder.yudao.framework.security.core.context.TransmittableThreadLocal import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter; import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl; import cn.iocoder.yudao.framework.security.core.handler.AuthenticationEntryPointImpl; +import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService; +import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkServiceImpl; import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; import cn.iocoder.yudao.module.system.api.auth.OAuth2TokenApi; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; import org.springframework.beans.factory.config.MethodInvokingFactoryBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -78,6 +81,11 @@ public class YudaoSecurityAutoConfiguration { return new TokenAuthenticationFilter(securityProperties, globalExceptionHandler, oauth2TokenApi); } + @Bean("ss") // 使用 Spring Security 的缩写,方便食用 + public SecurityFrameworkService securityFrameworkService(PermissionApi permissionApi) { + return new SecurityFrameworkServiceImpl(permissionApi); + } + /** * 声明调用 {@link SecurityContextHolder#setStrategyName(String)} 方法, * 设置使用 {@link TransmittableThreadLocalSecurityContextHolderStrategy} 作为 Security 的上下文策略 diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkService.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkService.java new file mode 100644 index 0000000..49cc9f9 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkService.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.framework.security.core.service; + +/** + * Security 框架 Service 接口,定义权限相关的校验操作 + * + * @author 芋道源码 + */ +public interface SecurityFrameworkService { + + /** + * 判断是否有权限 + * + * @param permission 权限 + * @return 是否 + */ + boolean hasPermission(String permission); + + /** + * 判断是否有权限,任一一个即可 + * + * @param permissions 权限 + * @return 是否 + */ + boolean hasAnyPermissions(String... permissions); + + /** + * 判断是否有角色 + * + * 注意,角色使用的是 SysRoleDO 的 code 标识 + * + * @param role 角色 + * @return 是否 + */ + boolean hasRole(String role); + + /** + * 判断是否有角色,任一一个即可 + * + * @param roles 角色数组 + * @return 是否 + */ + boolean hasAnyRoles(String... roles); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java new file mode 100644 index 0000000..2227ad3 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.framework.security.core.service; + +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import lombok.AllArgsConstructor; + +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +/** + * 默认的 {@link SecurityFrameworkService} 实现类 + * + * @author 芋道源码 + */ +@AllArgsConstructor +public class SecurityFrameworkServiceImpl implements SecurityFrameworkService { + + private final PermissionApi permissionApi; + + @Override + public boolean hasPermission(String permission) { + return hasAnyPermissions(permission); + } + + @Override + public boolean hasAnyPermissions(String... permissions) { + return permissionApi.hasAnyPermissions(getLoginUserId(), permissions); + } + + @Override + public boolean hasRole(String role) { + return hasAnyRoles(role); + } + + @Override + public boolean hasAnyRoles(String... roles) { + return permissionApi.hasAnyRoles(getLoginUserId(), roles); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityPermissionFrameworkService.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityPermissionFrameworkService.java deleted file mode 100644 index 75f11b1..0000000 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityPermissionFrameworkService.java +++ /dev/null @@ -1,44 +0,0 @@ -package cn.iocoder.yudao.framework.security.core.service; - -/** - * Security 框架 Permission Service 接口,定义 security 组件需要的功能 - * - * @author 芋道源码 - */ -public interface SecurityPermissionFrameworkService { - - /** - * 判断是否有权限 - * - * @param permission 权限 - * @return 是否 - */ - boolean hasPermission(String permission); - - /** - * 判断是否有权限,任一一个即可 - * - * @param permissions 权限 - * @return 是否 - */ - boolean hasAnyPermissions(String... permissions); - - /** - * 判断是否有角色 - * - * 注意,角色使用的是 SysRoleDO 的 code 标识 - * - * @param role 角色 - * @return 是否 - */ - boolean hasRole(String role); - - /** - * 判断是否有角色,任一一个即可 - * - * @param roles 角色数组 - * @return 是否 - */ - boolean hasAnyRoles(String... roles); - -} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApi.java index 04e9638..ea16fe4 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApi.java +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApi.java @@ -1,5 +1,7 @@ package cn.iocoder.yudao.module.system.api.permission; +import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO; + import java.util.Collection; import java.util.Set; @@ -18,4 +20,30 @@ public interface PermissionApi { */ Set getUserRoleIdListByRoleIds(Collection roleIds); + /** + * 判断是否有权限,任一一个即可 + * + * @param userId 用户编号 + * @param permissions 权限 + * @return 是否 + */ + boolean hasAnyPermissions(Long userId, String... permissions); + + /** + * 判断是否有角色,任一一个即可 + * + * @param userId 用户编号 + * @param roles 角色数组 + * @return 是否 + */ + boolean hasAnyRoles(Long userId, String... roles); + + /** + * 获得登陆用户的部门数据权限 + * + * @param userId 用户编号 + * @return 部门数据权限 + */ + DeptDataPermissionRespDTO getDeptDataPermission(Long userId); + } diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/dto/DeptDataPermissionRespDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/dto/DeptDataPermissionRespDTO.java new file mode 100644 index 0000000..5650e89 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/dto/DeptDataPermissionRespDTO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.system.api.permission.dto; + +import lombok.Data; + +import java.util.HashSet; +import java.util.Set; + +/** + * 部门的数据权限 Response DTO + * + * @author 芋道源码 + */ +@Data +public class DeptDataPermissionRespDTO { + + /** + * 是否可查看全部数据 + */ + private Boolean all; + /** + * 是否可查看自己的数据 + */ + private Boolean self; + /** + * 可查看的部门编号数组 + */ + private Set deptIds; + + public DeptDataPermissionRespDTO() { + this.all = false; + this.self = false; + this.deptIds = new HashSet<>(); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApiImpl.java index 5d2b550..52e30ea 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApiImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApiImpl.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.system.api.permission; +import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO; import cn.iocoder.yudao.module.system.service.permission.PermissionService; import org.springframework.stereotype.Service; @@ -23,4 +24,19 @@ public class PermissionApiImpl implements PermissionApi { return permissionService.getUserRoleIdListByRoleIds(roleIds); } + @Override + public boolean hasAnyPermissions(Long userId, String... permissions) { + return permissionService.hasAnyPermissions(userId, permissions); + } + + @Override + public boolean hasAnyRoles(Long userId, String... roles) { + return permissionService.hasAnyRoles(userId, roles); + } + + @Override + public DeptDataPermissionRespDTO getDeptDataPermission(Long userId) { + return permissionService.getDeptDataPermission(userId); + } + } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/datapermission/config/DataPermissionConfiguration.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/datapermission/config/DataPermissionConfiguration.java index d7246f2..136866c 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/datapermission/config/DataPermissionConfiguration.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/datapermission/config/DataPermissionConfiguration.java @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.system.framework.datapermission.config; import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; -import cn.iocoder.yudao.framework.datapermission.core.dept.rule.DeptDataPermissionRuleCustomizer; +import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionService.java index beee094..97174d6 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionService.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionService.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.system.service.permission; -import cn.iocoder.yudao.framework.datapermission.core.dept.service.DeptDataPermissionFrameworkService; -import cn.iocoder.yudao.framework.security.core.service.SecurityPermissionFrameworkService; +import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; import org.springframework.lang.Nullable; @@ -16,7 +15,7 @@ import java.util.Set; * * @author 芋道源码 */ -public interface PermissionService extends SecurityPermissionFrameworkService, DeptDataPermissionFrameworkService { +public interface PermissionService { /** * 初始化权限的本地缓存 @@ -115,4 +114,29 @@ public interface PermissionService extends SecurityPermissionFrameworkService, D */ void processUserDeleted(Long userId); + /** + * 判断是否有权限,任一一个即可 + * + * @param userId 用户编号 + * @param permissions 权限 + * @return 是否 + */ + boolean hasAnyPermissions(Long userId, String... permissions); + + /** + * 判断是否有角色,任一一个即可 + * + * @param roles 角色数组 + * @return 是否 + */ + boolean hasAnyRoles(Long userId, String... roles); + + /** + * 获得登陆用户的部门数据权限 + * + * @param userId 用户编号 + * @return 部门数据权限 + */ + DeptDataPermissionRespDTO getDeptDataPermission(Long userId); + } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java index 294c9c9..74435cf 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java @@ -8,7 +8,7 @@ import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.collection.MapUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; -import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO; +import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; @@ -45,7 +45,6 @@ import java.util.function.Supplier; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getMaxValue; -import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; import static java.util.Collections.singleton; /** @@ -53,7 +52,7 @@ import static java.util.Collections.singleton; * * @author 芋道源码 */ -@Service("ss") // 使用 Spring Security 的缩写,方便食用 +@Service @Slf4j public class PermissionServiceImpl implements PermissionService { @@ -71,7 +70,7 @@ public class PermissionServiceImpl implements PermissionService { * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 */ @Getter - @Setter // 单元测试 + @Setter // 单元测试需要 private volatile Multimap roleMenuCache; /** * 菜单编号与角色编号的缓存映射 @@ -81,6 +80,7 @@ public class PermissionServiceImpl implements PermissionService { * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 */ @Getter + @Setter // 单元测试需要 private volatile Multimap menuRoleCache; /** * 缓存 RoleMenu 的最大更新时间,用于后续的增量轮询,判断是否有更新 @@ -399,19 +399,14 @@ public class PermissionServiceImpl implements PermissionService { } @Override - public boolean hasPermission(String permission) { - return hasAnyPermissions(permission); - } - - @Override - public boolean hasAnyPermissions(String... permissions) { + public boolean hasAnyPermissions(Long userId, String... permissions) { // 如果为空,说明已经有权限 if (ArrayUtil.isEmpty(permissions)) { return true; } // 获得当前登录的角色。如果为空,说明没有权限 - Set roleIds = getUserRoleIdsFromCache(getLoginUserId(), singleton(CommonStatusEnum.ENABLE.getStatus())); + Set roleIds = getUserRoleIdsFromCache(userId, singleton(CommonStatusEnum.ENABLE.getStatus())); if (CollUtil.isEmpty(roleIds)) { return false; } @@ -434,19 +429,14 @@ public class PermissionServiceImpl implements PermissionService { } @Override - public boolean hasRole(String role) { - return hasAnyRoles(role); - } - - @Override - public boolean hasAnyRoles(String... roles) { + public boolean hasAnyRoles(Long userId, String... roles) { // 如果为空,说明已经有权限 if (ArrayUtil.isEmpty(roles)) { return true; } // 获得当前登录的角色。如果为空,说明没有权限 - Set roleIds = getUserRoleIdsFromCache(getLoginUserId(), singleton(CommonStatusEnum.ENABLE.getStatus())); + Set roleIds = getUserRoleIdsFromCache(userId, singleton(CommonStatusEnum.ENABLE.getStatus())); if (CollUtil.isEmpty(roleIds)) { return false; } diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AuthServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AuthServiceImplTest.java index a04a971..b68ef20 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AuthServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AuthServiceImplTest.java @@ -13,6 +13,7 @@ import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; import cn.iocoder.yudao.module.system.service.common.CaptchaService; import cn.iocoder.yudao.module.system.service.logger.LoginLogService; +import cn.iocoder.yudao.module.system.service.member.MemberService; import cn.iocoder.yudao.module.system.service.social.SocialUserService; import cn.iocoder.yudao.module.system.service.user.AdminUserService; import org.junit.jupiter.api.BeforeEach; @@ -49,6 +50,8 @@ public class AuthServiceImplTest extends BaseDbUnitTest { private SmsCodeApi smsCodeApi; @MockBean private OAuth2TokenService oauth2TokenService; + @MockBean + private MemberService memberService; @MockBean private Validator validator; diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java index d2ef99e..324a415 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java @@ -4,7 +4,7 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.map.MapUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; -import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO; +import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; @@ -359,22 +359,84 @@ public class PermissionServiceTest extends BaseDbUnitTest { verify(permissionProducer).sendUserRoleRefreshMessage(); } -// @Test -// public void testHasAnyRoles_superAdmin() { -// // 准备参数 -// String[] roles = new String[]{"yunai", "tudou"}; -// // mock 方法 -// List roleList = singletonList(randomPojo(RoleDO.class, o -> o.setId(100L))); -// when(roleService.getRolesFromCache(eq(roleIds))).thenReturn(roleList); -// when(roleService.hasAnySuperAdmin(same(roleList))).thenReturn(true); -// List menuList = randomPojoList(MenuDO.class); -// when(menuService.getMenuListFromCache(eq(menuTypes), eq(menusStatuses))).thenReturn(menuList); -// -// // 调用 -// List result = permissionService.getRoleMenuListFromCache(roleIds, menuTypes, menusStatuses); -// // 断言 -// assertSame(menuList, result); -// } + @Test + public void testHasAnyPermissions_superAdmin() { + // 准备参数 + Long userId = 1L; + String[] roles = new String[]{"system:user:query", "system:user:create"}; + // mock 用户与角色的缓存 + permissionService.setUserRoleCache(MapUtil.>builder().put(1L, asSet(100L)).build()); + RoleDO role = randomPojo(RoleDO.class, o -> o.setId(100L) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRoleFromCache(eq(100L))).thenReturn(role); + // mock 其它方法 + when(roleService.hasAnySuperAdmin(eq(asSet(100L)))).thenReturn(true); + + // 调用 + boolean has = permissionService.hasAnyPermissions(userId, roles); + // 断言 + assertTrue(has); + } + + @Test + public void testHasAnyPermissions_normal() { + // 准备参数 + Long userId = 1L; + String[] roles = new String[]{"system:user:query", "system:user:create"}; + // mock 用户与角色的缓存 + permissionService.setUserRoleCache(MapUtil.>builder().put(1L, asSet(100L)).build()); + RoleDO role = randomPojo(RoleDO.class, o -> o.setId(100L) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRoleFromCache(eq(100L))).thenReturn(role); + // mock 其它方法 + MenuDO menu = randomPojo(MenuDO.class, o -> o.setId(1000L)); + when(menuService.getMenuListByPermissionFromCache(eq("system:user:create"))).thenReturn(singletonList(menu)); + permissionService.setMenuRoleCache(ImmutableMultimap.builder().put(1000L, 100L).build()); + + + // 调用 + boolean has = permissionService.hasAnyPermissions(userId, roles); + // 断言 + assertTrue(has); + } + + @Test + public void testHasAnyRoles_superAdmin() { + // 准备参数 + Long userId = 1L; + String[] roles = new String[]{"yunai", "tudou"}; + // mock 用户与角色的缓存 + permissionService.setUserRoleCache(MapUtil.>builder().put(1L, asSet(100L)).build()); + RoleDO role = randomPojo(RoleDO.class, o -> o.setId(100L) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRoleFromCache(eq(100L))).thenReturn(role); + // mock 其它方法 + when(roleService.hasAnySuperAdmin(eq(asSet(100L)))).thenReturn(true); + + // 调用 + boolean has = permissionService.hasAnyRoles(userId, roles); + // 断言 + assertTrue(has); + } + + @Test + public void testHasAnyRoles_normal() { + // 准备参数 + Long userId = 1L; + String[] roles = new String[]{"yunai", "tudou"}; + // mock 用户与角色的缓存 + permissionService.setUserRoleCache(MapUtil.>builder().put(1L, asSet(100L)).build()); + RoleDO role = randomPojo(RoleDO.class, o -> o.setId(100L).setCode("yunai") + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRoleFromCache(eq(100L))).thenReturn(role); + // mock 其它方法 + when(roleService.getRolesFromCache(eq(asSet(100L)))).thenReturn(singletonList(role)); + + // 调用 + boolean has = permissionService.hasAnyRoles(userId, roles); + // 断言 + assertTrue(has); + } @Test public void testGetDeptDataPermission_All() { -- libgit2 0.21.4