Merge remote-tracking branch 'base-version/main' into dev
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
# 数据权限忽略与上下文覆盖说明
|
||||
|
||||
本文说明新增的公司/部门数据权限忽略能力,以及部门上下文对数据权限的覆盖策略。
|
||||
|
||||
## 新增注解
|
||||
|
||||
- `@CompanyDataPermissionIgnore(enable = "true")`
|
||||
- `@DeptDataPermissionIgnore(enable = "true")`
|
||||
|
||||
用法:
|
||||
- 可标记在类或方法上。
|
||||
- `enable` 支持 Spring EL,计算结果为 `true` 时生效,默认开启。
|
||||
- 生效后,在方法执行期间临时设置忽略标记,结束后自动恢复。
|
||||
|
||||
## 忽略生效范围
|
||||
|
||||
- 公司数据权限:切面在进入方法时将 `CompanyContextHolder.setIgnore(true)`,数据权限规则检测到后直接放行。
|
||||
- 部门数据权限:切面在进入方法时将 `DeptContextHolder.setIgnore(true)`,部门数据权限规则检测到后直接放行。
|
||||
|
||||
## 部门上下文覆盖策略
|
||||
|
||||
当未忽略部门数据权限且上下文存在有效部门 ID 时:
|
||||
- 优先使用上下文中的单一部门作为过滤条件,避免因默认的 `ALL` 或“无部门且不可查看自己”导致放行或误判无权。
|
||||
- 上下文部门不会修改原有的数据权限 DTO,仅在当前计算中使用。
|
||||
- 若上下文公司与缓存公司不一致,会记录告警日志,但仍按上下文部门过滤。
|
||||
|
||||
## 典型场景
|
||||
|
||||
1) **任务/全局调用需要暂时关闭数据权限**
|
||||
- 在方法上标记 `@DeptDataPermissionIgnore` 或 `@CompanyDataPermissionIgnore`。
|
||||
|
||||
2) **带部门上下文的接口调用**
|
||||
- 请求预先设置 `DeptContextHolder.setContext(deptId, companyId)`。
|
||||
- 即便数据权限声明为 `all=true`,也会按该部门过滤,避免读出全量。
|
||||
|
||||
3) **无部门权限但指定了上下文部门**
|
||||
- 即使 `deptIds` 为空且 `self=false`,只要上下文提供部门,也会使用该部门过滤,而非直接判定无权。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 忽略标记只作用于当前线程上下文,切面会在 `finally` 中恢复旧值,嵌套调用安全。
|
||||
- 若需要同时忽略公司与部门数据权限,可叠加两个注解或在业务代码中分别设置忽略标记。
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.zt.plat.framework.datapermission.config;
|
||||
|
||||
import com.zt.plat.framework.datapermission.core.aop.CompanyDataPermissionIgnoreAspect;
|
||||
import com.zt.plat.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor;
|
||||
import com.zt.plat.framework.datapermission.core.aop.DeptDataPermissionIgnoreAspect;
|
||||
import com.zt.plat.framework.datapermission.core.db.DataPermissionRuleHandler;
|
||||
import com.zt.plat.framework.datapermission.core.rule.DataPermissionRule;
|
||||
import com.zt.plat.framework.datapermission.core.rule.DataPermissionRuleFactory;
|
||||
@@ -43,4 +45,14 @@ public class ZtDataPermissionAutoConfiguration {
|
||||
return new DataPermissionAnnotationAdvisor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DeptDataPermissionIgnoreAspect deptDataPermissionIgnoreAspect() {
|
||||
return new DeptDataPermissionIgnoreAspect();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CompanyDataPermissionIgnoreAspect companyDataPermissionIgnoreAspect() {
|
||||
return new CompanyDataPermissionIgnoreAspect();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.zt.plat.framework.datapermission.core.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 忽略公司数据权限的注解。
|
||||
* <p>
|
||||
* 标记在方法或类上时,匹配的调用会临时忽略公司类型的数据权限规则。
|
||||
*/
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Inherited
|
||||
public @interface CompanyDataPermissionIgnore {
|
||||
|
||||
/**
|
||||
* 是否开启忽略,默认开启。
|
||||
* 支持 Spring EL 表达式,返回 true 时生效。
|
||||
*/
|
||||
String enable() default "true";
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.zt.plat.framework.datapermission.core.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 忽略部门数据权限的注解。
|
||||
* <p>
|
||||
* 标记在方法或类上时,匹配的调用会临时忽略部门类型的数据权限规则。
|
||||
*/
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Inherited
|
||||
public @interface DeptDataPermissionIgnore {
|
||||
|
||||
/**
|
||||
* 是否开启忽略,默认开启。
|
||||
* 支持 Spring EL 表达式,返回 true 时生效。
|
||||
*/
|
||||
String enable() default "true";
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.zt.plat.framework.datapermission.core.aop;
|
||||
|
||||
import com.zt.plat.framework.common.util.spring.SpringExpressionUtils;
|
||||
import com.zt.plat.framework.datapermission.core.annotation.CompanyDataPermissionIgnore;
|
||||
import com.zt.plat.framework.tenant.core.context.CompanyContextHolder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
|
||||
/**
|
||||
* 公司数据权限忽略切面,基于 {@link CompanyDataPermissionIgnore} 注解。
|
||||
*/
|
||||
@Aspect
|
||||
@Slf4j
|
||||
public class CompanyDataPermissionIgnoreAspect {
|
||||
|
||||
@Around("@within(companyDataPermissionIgnore) || @annotation(companyDataPermissionIgnore)")
|
||||
public Object around(ProceedingJoinPoint joinPoint, CompanyDataPermissionIgnore companyDataPermissionIgnore) throws Throwable {
|
||||
boolean oldIgnore = CompanyContextHolder.isIgnore();
|
||||
try {
|
||||
if (companyDataPermissionIgnore == null) {
|
||||
Class<?> targetClass = joinPoint.getTarget().getClass();
|
||||
companyDataPermissionIgnore = targetClass.getAnnotation(CompanyDataPermissionIgnore.class);
|
||||
}
|
||||
Object enable = SpringExpressionUtils.parseExpression(companyDataPermissionIgnore.enable());
|
||||
if (Boolean.TRUE.equals(enable)) {
|
||||
CompanyContextHolder.setIgnore(true);
|
||||
}
|
||||
return joinPoint.proceed();
|
||||
} finally {
|
||||
CompanyContextHolder.setIgnore(oldIgnore);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.zt.plat.framework.datapermission.core.aop;
|
||||
|
||||
import com.zt.plat.framework.common.util.spring.SpringExpressionUtils;
|
||||
import com.zt.plat.framework.datapermission.core.annotation.DeptDataPermissionIgnore;
|
||||
import com.zt.plat.framework.tenant.core.context.DeptContextHolder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
|
||||
/**
|
||||
* 部门数据权限忽略切面,基于 {@link DeptDataPermissionIgnore} 注解。
|
||||
*/
|
||||
@Aspect
|
||||
@Slf4j
|
||||
public class DeptDataPermissionIgnoreAspect {
|
||||
|
||||
@Around("@within(deptDataPermissionIgnore) || @annotation(deptDataPermissionIgnore)")
|
||||
public Object around(ProceedingJoinPoint joinPoint, DeptDataPermissionIgnore deptDataPermissionIgnore) throws Throwable {
|
||||
boolean oldIgnore = DeptContextHolder.shouldIgnore();
|
||||
try {
|
||||
if (deptDataPermissionIgnore == null) {
|
||||
Class<?> targetClass = joinPoint.getTarget().getClass();
|
||||
deptDataPermissionIgnore = targetClass.getAnnotation(DeptDataPermissionIgnore.class);
|
||||
}
|
||||
Object enable = SpringExpressionUtils.parseExpression(deptDataPermissionIgnore.enable());
|
||||
if (Boolean.TRUE.equals(enable)) {
|
||||
DeptContextHolder.setIgnore(true);
|
||||
}
|
||||
return joinPoint.proceed();
|
||||
} finally {
|
||||
DeptContextHolder.setIgnore(oldIgnore);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,9 @@ import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
|
||||
import net.sf.jsqlparser.schema.Table;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.zt.plat.framework.security.core.util.SecurityFrameworkUtils.skipPermissionCheck;
|
||||
|
||||
@@ -41,6 +43,7 @@ public class DataPermissionRuleHandler implements MultiDataPermissionHandler {
|
||||
}
|
||||
|
||||
// 生成条件
|
||||
final Set<String> processed = new HashSet<>();
|
||||
Expression allExpression = null;
|
||||
for (DataPermissionRule rule : rules) {
|
||||
// 判断表名是否匹配
|
||||
@@ -49,6 +52,14 @@ public class DataPermissionRuleHandler implements MultiDataPermissionHandler {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 同一张表 + 同一别名 + 同一规则 在一次 SQL 解析内仅处理一次,避免重复拼接条件
|
||||
String aliasName = table.getAlias() == null ? "" : table.getAlias().getName();
|
||||
String key = tableName + "|" + aliasName + "|" + rule.getClass().getName();
|
||||
if (processed.contains(key)) {
|
||||
continue;
|
||||
}
|
||||
processed.add(key);
|
||||
|
||||
// 单条规则的条件
|
||||
Expression oneExpress = rule.getExpression(tableName, table.getAlias());
|
||||
if (oneExpress == null) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import cn.hutool.core.util.StrUtil;
|
||||
import com.zt.plat.framework.common.util.collection.CollectionUtils;
|
||||
import com.zt.plat.framework.datapermission.core.rule.DataPermissionRule;
|
||||
import com.zt.plat.framework.mybatis.core.util.MyBatisUtils;
|
||||
import com.zt.plat.framework.tenant.core.context.CompanyContextHolder;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.expression.Alias;
|
||||
@@ -49,6 +50,10 @@ public class CompanyDataPermissionRule implements DataPermissionRule {
|
||||
|
||||
@Override
|
||||
public Expression getExpression(String tableName, Alias tableAlias) {
|
||||
// 显式忽略公司数据权限时直接放行
|
||||
if (CompanyContextHolder.isIgnore()) {
|
||||
return null;
|
||||
}
|
||||
// 业务拼接 Company 的条件
|
||||
if (getLoginUserCompanyId() == null) {
|
||||
// 如果没有登录用户的公司编号,则不需要拼接条件
|
||||
|
||||
@@ -156,21 +156,28 @@ public class DeptDataPermissionRule implements DataPermissionRule {
|
||||
}
|
||||
}
|
||||
|
||||
// 情况一,如果是 ALL 可查看全部,则无需拼接条件
|
||||
if (deptDataPermission.getAll()) {
|
||||
// 计算有效的部门与自查标记:当存在上下文部门且未被忽略时,强制仅使用该部门,以避免默认全量或空权限分支
|
||||
Set<Long> effectiveDeptIds = deptDataPermission.getDeptIds();
|
||||
Boolean effectiveSelf = deptDataPermission.getSelf();
|
||||
if (!DeptContextHolder.shouldIgnore() && ctxDeptId != null && ctxDeptId > 0L) {
|
||||
effectiveDeptIds = CollUtil.newHashSet(ctxDeptId);
|
||||
}
|
||||
|
||||
// 情况一:仅当不存在上下文部门时,且 ALL 可查看全部,才无需拼接条件;若存在上下文部门则仍需基于该部门过滤
|
||||
if (ctxDeptId == null && deptDataPermission.getAll()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限
|
||||
if (CollUtil.isEmpty(deptDataPermission.getDeptIds())
|
||||
&& Boolean.FALSE.equals(deptDataPermission.getSelf())) {
|
||||
// 情况二:仅在有效部门集合为空且不可查看自己时,才认为无权限;若上下文提供部门,则跳过该兜底
|
||||
if (CollUtil.isEmpty(effectiveDeptIds)
|
||||
&& Boolean.FALSE.equals(effectiveSelf)) {
|
||||
return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空
|
||||
}
|
||||
|
||||
// 情况三,拼接 Dept 和 Company User 的条件,最后组合
|
||||
Expression deptExpression = buildDeptExpression(tableName, tableAlias, deptDataPermission.getDeptIds());
|
||||
Expression deptExpression = buildDeptExpression(tableName, tableAlias, effectiveDeptIds);
|
||||
// Expression deptExpression = buildDeptExpression(tableName, tableAlias, deptDataPermission.getDeptIds());
|
||||
Expression userExpression = buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId());
|
||||
Expression userExpression = buildUserExpression(tableName, tableAlias, effectiveSelf, loginUser.getId());
|
||||
if (deptExpression == null && userExpression == null) {
|
||||
// TODO ZT:获得不到条件的时候,暂时不抛出异常,而是不返回数据
|
||||
log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]",
|
||||
|
||||
@@ -264,7 +264,7 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test // 上下文部门存在且公司一致时,清空原集合并覆盖为单一 deptId
|
||||
@Test // 上下文部门存在且公司一致时,表达式按上下文 deptId 生效,但不修改原数据权限集合
|
||||
void testGetExpression_deptContextOverride_companyMatch() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> secMock = mockStatic(SecurityFrameworkUtils.class);
|
||||
MockedStatic<DeptContextHolder> deptCtxMock = mockStatic(DeptContextHolder.class);
|
||||
@@ -292,12 +292,13 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest {
|
||||
Expression expression = rule.getExpression(tableName, tableAlias);
|
||||
|
||||
assertEquals("u.dept_id IN (99)", expression.toString());
|
||||
assertEquals(CollUtil.newLinkedHashSet(99L), deptDataPermission.getDeptIds());
|
||||
// 原始权限对象不被修改,只是临时使用上下文 deptId 计算
|
||||
assertEquals(CollUtil.newLinkedHashSet(10L, 20L), deptDataPermission.getDeptIds());
|
||||
assertEquals(1L, deptDataPermission.getCompanyId());
|
||||
}
|
||||
}
|
||||
|
||||
@Test // 上下文部门存在但公司不一致时,记录告警并保持原逻辑(不覆盖)
|
||||
@Test // 上下文部门存在但公司不一致时,仍按上下文 deptId 过滤,原数据权限保持不变
|
||||
void testGetExpression_deptContextOverride_companyMismatch() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> secMock = mockStatic(SecurityFrameworkUtils.class);
|
||||
MockedStatic<DeptContextHolder> deptCtxMock = mockStatic(DeptContextHolder.class);
|
||||
@@ -324,10 +325,72 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest {
|
||||
|
||||
Expression expression = rule.getExpression(tableName, tableAlias);
|
||||
|
||||
assertEquals("u.dept_id IN (10)", expression.toString());
|
||||
assertEquals(CollUtil.newLinkedHashSet(10L), deptDataPermission.getDeptIds());
|
||||
assertEquals(1L, deptDataPermission.getCompanyId());
|
||||
assertEquals("u.dept_id IN (99)", expression.toString());
|
||||
// 原始权限对象不被修改
|
||||
assertEquals(CollUtil.newLinkedHashSet(10L), deptDataPermission.getDeptIds());
|
||||
assertEquals(1L, deptDataPermission.getCompanyId());
|
||||
}
|
||||
}
|
||||
|
||||
@Test // ALL 权限但存在上下文部门时,仍按上下文部门过滤
|
||||
void testGetExpression_allPermission_withCtxDept() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> secMock = mockStatic(SecurityFrameworkUtils.class);
|
||||
MockedStatic<DeptContextHolder> deptCtxMock = mockStatic(DeptContextHolder.class);
|
||||
MockedStatic<CompanyContextHolder> companyCtxMock = mockStatic(CompanyContextHolder.class)) {
|
||||
|
||||
String tableName = "t_user";
|
||||
Alias tableAlias = new Alias("u");
|
||||
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
|
||||
.setUserType(UserTypeEnum.ADMIN.getValue()));
|
||||
secMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
|
||||
|
||||
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
|
||||
.setAll(true)
|
||||
.setDeptIds(CollUtil.newLinkedHashSet(10L));
|
||||
when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(success(deptDataPermission));
|
||||
|
||||
deptCtxMock.when(DeptContextHolder::shouldIgnore).thenReturn(false);
|
||||
deptCtxMock.when(DeptContextHolder::getDeptId).thenReturn(99L);
|
||||
deptCtxMock.when(DeptContextHolder::getCompanyId).thenReturn(1L);
|
||||
companyCtxMock.when(CompanyContextHolder::getCompanyId).thenReturn(1L);
|
||||
companyCtxMock.when(CompanyContextHolder::isIgnore).thenReturn(false);
|
||||
|
||||
rule.addDeptColumn(tableName, "dept_id");
|
||||
|
||||
Expression expression = rule.getExpression(tableName, tableAlias);
|
||||
|
||||
assertEquals("u.dept_id IN (99)", expression.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Test // 无部门且不可查看自己,但上下文提供部门时,应使用上下文部门而非判定无权限
|
||||
void testGetExpression_noDeptNoSelf_withCtxDept() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> secMock = mockStatic(SecurityFrameworkUtils.class);
|
||||
MockedStatic<DeptContextHolder> deptCtxMock = mockStatic(DeptContextHolder.class);
|
||||
MockedStatic<CompanyContextHolder> companyCtxMock = mockStatic(CompanyContextHolder.class)) {
|
||||
|
||||
String tableName = "t_user";
|
||||
Alias tableAlias = new Alias("u");
|
||||
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
|
||||
.setUserType(UserTypeEnum.ADMIN.getValue()));
|
||||
secMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
|
||||
|
||||
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
|
||||
.setSelf(false);
|
||||
when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(success(deptDataPermission));
|
||||
|
||||
deptCtxMock.when(DeptContextHolder::shouldIgnore).thenReturn(false);
|
||||
deptCtxMock.when(DeptContextHolder::getDeptId).thenReturn(88L);
|
||||
deptCtxMock.when(DeptContextHolder::getCompanyId).thenReturn(1L);
|
||||
companyCtxMock.when(CompanyContextHolder::getCompanyId).thenReturn(1L);
|
||||
companyCtxMock.when(CompanyContextHolder::isIgnore).thenReturn(false);
|
||||
|
||||
rule.addDeptColumn(tableName, "dept_id");
|
||||
|
||||
Expression expression = rule.getExpression(tableName, tableAlias);
|
||||
|
||||
assertEquals("u.dept_id IN (88)", expression.toString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user