Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
5
pom.xml
5
pom.xml
@@ -32,7 +32,7 @@
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<properties>
|
||||
<revision>3.0.45</revision>
|
||||
<revision>3.0.46</revision>
|
||||
<!-- Maven 相关 -->
|
||||
<java.version>17</java.version>
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
@@ -271,7 +271,8 @@
|
||||
<profile>
|
||||
<id>chenbowen</id>
|
||||
<properties>
|
||||
<env.name>local</env.name>
|
||||
<!-- <env.name>local</env.name>-->
|
||||
<env.name>dev</env.name>
|
||||
<!-- <config.server-addr>localhost:8848</config.server-addr>-->
|
||||
<config.server-addr>172.16.46.63:30848</config.server-addr>
|
||||
<config.namespace>chenbowen</config.namespace>
|
||||
|
||||
@@ -19,7 +19,8 @@ CREATE TABLE databus_api_definition_credential (
|
||||
deleted BIT DEFAULT '0' NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uk_databus_api_definition_credential ON databus_api_definition_credential (api_id, credential_id, deleted);
|
||||
-- 去掉错误的唯一索引逻辑
|
||||
-- CREATE UNIQUE INDEX uk_databus_api_definition_credential ON databus_api_definition_credential (api_id, credential_id, deleted);
|
||||
CREATE INDEX idx_databus_api_definition_credential_api ON databus_api_definition_credential (api_id);
|
||||
CREATE INDEX idx_databus_api_definition_credential_cred ON databus_api_definition_credential (credential_id);
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<properties>
|
||||
<revision>3.0.45</revision>
|
||||
<revision>3.0.46</revision>
|
||||
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
|
||||
<!-- 统一依赖管理 -->
|
||||
<spring.boot.version>3.4.5</spring.boot.version>
|
||||
|
||||
@@ -28,6 +28,14 @@ import java.util.*;
|
||||
@Slf4j
|
||||
public class BusinessDataPermissionEntityScanner {
|
||||
|
||||
/**
|
||||
* 临时排除的包前缀(物流模块 DO,不参与数据权限扫描)
|
||||
*/
|
||||
private static final Set<String> EXCLUDED_PACKAGE_PREFIXES = Set.of(
|
||||
"com.zt.plat.module.backendlogistics",
|
||||
"com.zt.plat.module.erp",
|
||||
"com.zt.plat.framework.mybatis.core.dataobject.BusinessBaseDO");
|
||||
|
||||
private final Set<String> basePackages;
|
||||
private final ClassLoader classLoader;
|
||||
|
||||
@@ -70,6 +78,9 @@ public class BusinessDataPermissionEntityScanner {
|
||||
if (!StringUtils.hasText(className)) {
|
||||
continue;
|
||||
}
|
||||
if (isExcludedPackage(className)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
Class<?> clazz = ClassUtils.forName(className, classLoader);
|
||||
if (clazz == BusinessBaseDO.class || !BusinessBaseDO.class.isAssignableFrom(clazz)) {
|
||||
@@ -92,6 +103,15 @@ public class BusinessDataPermissionEntityScanner {
|
||||
return new ArrayList<>(metadataMap.values());
|
||||
}
|
||||
|
||||
private boolean isExcludedPackage(String className) {
|
||||
for (String prefix : EXCLUDED_PACKAGE_PREFIXES) {
|
||||
if (className.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private EntityMetadata buildMetadata(Class<? extends BusinessBaseDO> entityClass) {
|
||||
String tableName = resolveTableName(entityClass);
|
||||
if (!StringUtils.hasText(tableName)) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.zt.plat.framework.datapermission.core.rule.dept;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.zt.plat.framework.common.biz.system.permission.PermissionCommonApi;
|
||||
import com.zt.plat.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO;
|
||||
import com.zt.plat.framework.common.enums.UserTypeEnum;
|
||||
@@ -14,7 +15,7 @@ import com.zt.plat.framework.mybatis.core.util.MyBatisUtils;
|
||||
import com.zt.plat.framework.security.core.LoginUser;
|
||||
import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.zt.plat.framework.tenant.core.context.CompanyContextHolder;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.zt.plat.framework.tenant.core.context.DeptContextHolder;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.expression.Alias;
|
||||
@@ -108,6 +109,11 @@ public class DeptDataPermissionRule implements DataPermissionRule {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 显式忽略部门数据权限时直接放行
|
||||
if (DeptContextHolder.shouldIgnore()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获得数据权限
|
||||
DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class);
|
||||
// 从上下文中拿不到,则调用逻辑进行获取
|
||||
@@ -136,6 +142,20 @@ public class DeptDataPermissionRule implements DataPermissionRule {
|
||||
}
|
||||
}
|
||||
|
||||
// 若存在部门上下文,优先使用上下文中的单一部门,必要时校验公司一致性
|
||||
Long ctxDeptId = DeptContextHolder.getDeptId();
|
||||
if (ctxDeptId != null && ctxDeptId > 0L) {
|
||||
Long currentCompanyId = CompanyContextHolder.getCompanyId();
|
||||
Long ctxCompanyId = DeptContextHolder.getCompanyId();
|
||||
Long compareCompanyId = ctxCompanyId != null ? ctxCompanyId : currentCompanyId;
|
||||
if (currentCompanyId != null && currentCompanyId > 0L
|
||||
&& compareCompanyId != null && !currentCompanyId.equals(compareCompanyId)) {
|
||||
log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptContextHolder company mismatch: currentCompanyId={}, ctxCompanyId={}, ctxDeptId={}, source=DeptContextHolder]",
|
||||
JsonUtils.toJsonString(loginUser), tableName, tableAlias == null ? null : tableAlias.getName(),
|
||||
currentCompanyId, compareCompanyId, ctxDeptId);
|
||||
}
|
||||
}
|
||||
|
||||
// 情况一,如果是 ALL 可查看全部,则无需拼接条件
|
||||
if (deptDataPermission.getAll()) {
|
||||
return null;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,61 @@
|
||||
package com.zt.plat.framework.tenant.core.context;
|
||||
|
||||
import com.alibaba.ttl.TransmittableThreadLocal;
|
||||
|
||||
/**
|
||||
* 部门上下文 Holder,使用 {@link TransmittableThreadLocal} 支持在线程池/异步场景下的上下文传递。
|
||||
*
|
||||
* 包含当前部门编号、所属公司编号以及是否忽略部门数据权限的标识。
|
||||
*/
|
||||
public class DeptContextHolder {
|
||||
|
||||
/** 当前部门编号 */
|
||||
private static final ThreadLocal<Long> DEPT_ID = new TransmittableThreadLocal<>();
|
||||
/** 当前部门所属公司编号(用于一致性校验) */
|
||||
private static final ThreadLocal<Long> COMPANY_ID = new TransmittableThreadLocal<>();
|
||||
/** 是否忽略部门数据权限 */
|
||||
private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>();
|
||||
|
||||
public static Long getDeptId() {
|
||||
return DEPT_ID.get();
|
||||
}
|
||||
|
||||
public static Long getCompanyId() {
|
||||
return COMPANY_ID.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置部门与所属公司编号。
|
||||
*/
|
||||
public static void setContext(Long deptId, Long companyId) {
|
||||
DEPT_ID.set(deptId);
|
||||
COMPANY_ID.set(companyId);
|
||||
}
|
||||
|
||||
public static void setDeptId(Long deptId) {
|
||||
DEPT_ID.set(deptId);
|
||||
}
|
||||
|
||||
public static void setCompanyId(Long companyId) {
|
||||
COMPANY_ID.set(companyId);
|
||||
}
|
||||
|
||||
public static boolean hasDeptId() {
|
||||
Long deptId = DEPT_ID.get();
|
||||
return deptId != null && deptId > 0L;
|
||||
}
|
||||
|
||||
public static void setIgnore(Boolean ignore) {
|
||||
IGNORE.set(ignore);
|
||||
}
|
||||
|
||||
public static boolean shouldIgnore() {
|
||||
return Boolean.TRUE.equals(IGNORE.get());
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
DEPT_ID.remove();
|
||||
COMPANY_ID.remove();
|
||||
IGNORE.remove();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.zt.plat.framework.tenant.core.web;
|
||||
import com.zt.plat.framework.security.core.LoginUser;
|
||||
import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.zt.plat.framework.tenant.core.context.CompanyContextHolder;
|
||||
import com.zt.plat.framework.tenant.core.context.DeptContextHolder;
|
||||
import com.zt.plat.framework.web.core.util.WebFrameworkUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -66,11 +67,19 @@ public class CompanyVisitContextInterceptor implements HandlerInterceptor {
|
||||
|
||||
if (companyId == null || companyId <= 0L) {
|
||||
CompanyContextHolder.setIgnore(true);
|
||||
DeptContextHolder.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
CompanyContextHolder.setIgnore(false);
|
||||
CompanyContextHolder.setCompanyId(companyId);
|
||||
// 默认不忽略部门数据权限;如果有有效部门则写入上下文
|
||||
DeptContextHolder.setIgnore(false);
|
||||
if (deptId != null && deptId > 0L) {
|
||||
DeptContextHolder.setContext(deptId, companyId);
|
||||
} else {
|
||||
DeptContextHolder.clear();
|
||||
}
|
||||
if (loginUser == null) {
|
||||
return true;
|
||||
}
|
||||
@@ -91,7 +100,9 @@ public class CompanyVisitContextInterceptor implements HandlerInterceptor {
|
||||
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
|
||||
if (loginUser != null) {
|
||||
loginUser.setVisitCompanyId(0L);
|
||||
loginUser.setVisitDeptId(0L);
|
||||
}
|
||||
DeptContextHolder.clear();
|
||||
}
|
||||
|
||||
private Long resolveLong(Object value) {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.zt.plat.framework.tenant.core.web;
|
||||
|
||||
import com.zt.plat.framework.security.core.LoginUser;
|
||||
import com.zt.plat.framework.tenant.core.context.CompanyContextHolder;
|
||||
import com.zt.plat.framework.tenant.core.context.DeptContextHolder;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* CompanyVisitContextInterceptor 单测,覆盖公司/部门上下文写入及清理。
|
||||
*/
|
||||
class CompanyVisitContextInterceptorTest {
|
||||
|
||||
private final HandlerInterceptor interceptor = new CompanyVisitContextInterceptor();
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
CompanyContextHolder.clear();
|
||||
DeptContextHolder.clear();
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test // 无公司 id:应 ignore,公司/部门上下文清空
|
||||
void testPreHandle_noCompanyId_ignore() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
boolean result = interceptor.preHandle(request, response, new Object());
|
||||
|
||||
assertTrue(result);
|
||||
assertTrue(CompanyContextHolder.isIgnore());
|
||||
assertNull(CompanyContextHolder.getCompanyId());
|
||||
assertNull(DeptContextHolder.getDeptId());
|
||||
}
|
||||
|
||||
@Test // 有公司无部门:写入公司,部门清空
|
||||
void testPreHandle_companyOnly() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
LoginUser loginUser = new LoginUser();
|
||||
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(loginUser, null));
|
||||
request.addHeader("visit-company-id", "11");
|
||||
|
||||
boolean result = interceptor.preHandle(request, response, new Object());
|
||||
|
||||
assertTrue(result);
|
||||
assertFalse(CompanyContextHolder.isIgnore());
|
||||
assertEquals(11L, CompanyContextHolder.getCompanyId());
|
||||
assertFalse(DeptContextHolder.shouldIgnore());
|
||||
assertNull(DeptContextHolder.getDeptId());
|
||||
assertEquals(11L, loginUser.getVisitCompanyId());
|
||||
assertNull(loginUser.getVisitDeptId());
|
||||
}
|
||||
|
||||
@Test // 有公司+部门:写入公司、部门上下文,afterCompletion 清理 visitDeptId & holder
|
||||
void testPreHandle_withCompanyAndDept_andAfterCompletionClear() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
LoginUser loginUser = new LoginUser();
|
||||
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(loginUser, null));
|
||||
request.addHeader("visit-company-id", "22");
|
||||
request.addHeader("visit-dept-id", "33");
|
||||
|
||||
boolean result = interceptor.preHandle(request, response, new Object());
|
||||
|
||||
assertTrue(result);
|
||||
assertFalse(CompanyContextHolder.isIgnore());
|
||||
assertEquals(22L, CompanyContextHolder.getCompanyId());
|
||||
assertEquals(33L, DeptContextHolder.getDeptId());
|
||||
assertEquals(22L, DeptContextHolder.getCompanyId());
|
||||
assertEquals(22L, loginUser.getVisitCompanyId());
|
||||
assertEquals(33L, loginUser.getVisitDeptId());
|
||||
|
||||
// afterCompletion: 清理 visitCompanyId/visitDeptId 与 holder
|
||||
interceptor.afterCompletion(request, response, new Object(), null);
|
||||
assertEquals(0L, loginUser.getVisitCompanyId());
|
||||
assertEquals(0L, loginUser.getVisitDeptId());
|
||||
assertNull(DeptContextHolder.getDeptId());
|
||||
assertNull(DeptContextHolder.getCompanyId());
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import org.springframework.web.util.ContentCachingResponseWrapper;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLDecoder;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -304,15 +305,28 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
|
||||
.build()
|
||||
.getQueryParams();
|
||||
params.forEach((key, values) -> {
|
||||
if (!StringUtils.hasText(key) || "signature".equalsIgnoreCase(key)) {
|
||||
String decodedKey = URLDecoder.decode(key, StandardCharsets.UTF_8);
|
||||
if (!StringUtils.hasText(decodedKey) || "signature".equalsIgnoreCase(decodedKey)) {
|
||||
return;
|
||||
}
|
||||
if (CollectionUtils.isEmpty(values)) {
|
||||
target.put(key, "");
|
||||
} else if (values.size() == 1) {
|
||||
target.put(key, values.get(0));
|
||||
target.put(decodedKey, "");
|
||||
return;
|
||||
}
|
||||
// 对每一个 value 做 URL 解码,确保与客户端原文签名一致
|
||||
List<String> decodedValues = values.stream()
|
||||
.map(val -> URLDecoder.decode(val, StandardCharsets.UTF_8))
|
||||
.toList();
|
||||
boolean allNullLiteral = decodedValues.stream()
|
||||
.allMatch(v -> "null".equals(v));
|
||||
if (allNullLiteral) {
|
||||
// 过滤掉仅包含字符串 "null" 的参数
|
||||
return;
|
||||
}
|
||||
if (decodedValues.size() == 1) {
|
||||
target.put(decodedKey, decodedValues.get(0));
|
||||
} else {
|
||||
target.put(key, String.join(",", values));
|
||||
target.put(decodedKey, String.join(",", decodedValues));
|
||||
}
|
||||
});
|
||||
} catch (IllegalArgumentException ex) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user