Merge branch 'refs/heads/zt-test' into test

# Conflicts:
#	zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/biz/system/permission/PermissionCommonApi.java
#	zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/context/DeptContextHolder.java
#	zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionServiceImpl.java
This commit is contained in:
FCL
2026-01-19 10:59:08 +08:00
178 changed files with 7735 additions and 273 deletions

View File

@@ -10,6 +10,7 @@
<modules>
<module>zt-module-databus-api</module>
<module>zt-module-databus-server</module>
<module>zt-module-databus-server-app</module>
</modules>
<modelVersion>4.0.0</modelVersion>

View File

@@ -0,0 +1,19 @@
## AdoptOpenJDK 停止发布 OpenJDK 二进制,而 Eclipse Temurin 是它的延伸,提供更好的稳定性
FROM 172.16.46.66:10043/base-service/eclipse-temurin:21-jre
## 创建目录,并使用它作为工作目录
RUN mkdir -p /zt-module-databus-server-app
WORKDIR /zt-module-databus-server-app
## 将后端项目的 Jar 文件,复制到镜像中
COPY ./target/zt-module-databus-server-app.jar app.jar
## 设置 TZ 时区
## 设置 JAVA_OPTS 环境变量,可通过 docker run -e "JAVA_OPTS=" 进行覆盖
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms512m -Xmx1024m"
## 暴露后端项目的 48080 端口
EXPOSE 48082
## 启动后端项目
CMD java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar app.jar

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>zt-module-databus</artifactId>
<groupId>com.zt.plat</groupId>
<version>${revision}</version>
</parent>
<artifactId>zt-module-databus-server-app</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>
Databus 模块启动器。
</description>
<dependencies>
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-module-databus-server</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
<build>
<!-- 设置构建的 jar 包名 -->
<finalName>${project.artifactId}</finalName>
<plugins>
<!-- 打包 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal> <!-- 将引入的 jar 打入其中 -->
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -37,17 +37,11 @@ spring:
primary: master
datasource:
master:
#url: jdbc:dm://172.16.46.247:1050?schema=RUOYI-VUE-PRO
#username: SYSDBA
#password: pgbsci6ddJ6Sqj@e
url: jdbc:dm://172.17.11.98:20870?schema=JYGK_TEST
username: SYSDBA
password: P@ssword25
slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改
lazy: true # 开启懒加载,保证启动速度
#url: jdbc:dm://172.16.46.247:1050?schema=RUOYI-VUE-PRO
#username: SYSDBA
#password: pgbsci6ddJ6Sqj@e
url: jdbc:dm://172.17.11.98:20870?schema=JYGK_TEST
username: SYSDBA
password: P@ssword25
@@ -58,8 +52,9 @@ spring:
host: 172.16.46.63 # 地址
port: 30379 # 端口
database: 0 # 数据库索引
username: zt-redis # 密码,建议生产环境开启
username: zt-redis
password: P@ssword25
# password: 123456 # 密码,建议生产环境开启
xxl:
job:

View File

@@ -192,24 +192,4 @@
</dependency>
</dependencies>
<build>
<!-- 设置构建的 jar 包名 -->
<finalName>${project.artifactId}</finalName>
<plugins>
<!-- 打包 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal> <!-- 将引入的 jar 打入其中 -->
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -42,6 +42,9 @@ public class ApiClientCredentialRespVO {
@Schema(description = "匿名访问固定用户昵称", example = "张三")
private String anonymousUserNickname;
@Schema(description = "是否启用加密", example = "true")
private Boolean enableEncryption;
@Schema(description = "创建时间")
private LocalDateTime createTime;

View File

@@ -45,4 +45,8 @@ public class ApiClientCredentialSaveReqVO {
@Schema(description = "匿名访问固定用户 ID", example = "1024")
private Long anonymousUserId;
@Schema(description = "是否启用加密", example = "true")
@NotNull(message = "启用加密标识不能为空")
private Boolean enableEncryption;
}

View File

@@ -38,4 +38,6 @@ public class ApiClientCredentialDO extends BaseDO {
private Long anonymousUserId;
private Boolean enableEncryption;
}

View File

@@ -108,7 +108,9 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
credential = credentialService.findActiveCredential(appId)
.orElseThrow(() -> new SecurityValidationException(HttpStatus.UNAUTHORIZED, "应用凭证不存在或已禁用"));
boolean allowAnonymous = Boolean.TRUE.equals(credential.getAllowAnonymous());
boolean enableEncryption = Boolean.TRUE.equals(credential.getEnableEncryption());
ApiAnonymousUserService.AnonymousUserDetails anonymousDetails = null;
byte[] requestBody = StreamUtils.copyToByteArray(request.getInputStream());
if (allowAnonymous) {
Long anonymousUserId = credential.getAnonymousUserId();
if (anonymousUserId == null) {
@@ -117,24 +119,25 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
anonymousDetails = anonymousUserService.find(anonymousUserId)
.orElseThrow(() -> new SecurityValidationException(HttpStatus.UNAUTHORIZED, "匿名访问固定用户不可用"));
}
String timestampHeader = requireHeader(request, TIMESTAMP_HEADER, "缺少时间戳");
// 校验时间戳与随机数,防止请求被重放
validateTimestamp(timestampHeader, security);
String nonce = requireHeader(request, NONCE_HEADER, "缺少随机数");
if (nonce.length() < 8) {
throw new SecurityValidationException(HttpStatus.BAD_REQUEST, "随机数长度不足");
}
String signature = requireHeader(request, SIGNATURE_HEADER, "缺少签名");
if (enableEncryption){
String nonce = requireHeader(request, NONCE_HEADER, "缺少随机数");
if (nonce.length() < 8) {
throw new SecurityValidationException(HttpStatus.BAD_REQUEST, "随机数长度不足");
}
String signature = requireHeader(request, SIGNATURE_HEADER, "缺少签名");
byte[] originalBody = StreamUtils.copyToByteArray(request.getInputStream());
// 尝试按凭证配置解密请求体,并构建签名载荷进行校验
byte[] decryptedBody = decryptRequestBody(originalBody, credential, security);
verifySignature(request, decryptedBody, signature, credential, security, appId, timestampHeader);
ensureNonce(tenantId, appId, nonce, security);
// 尝试按凭证配置解密请求体,并构建签名载荷进行校验
byte[] decryptedBody = decryptRequestBody(requestBody, credential, security);
verifySignature(request, decryptedBody, signature, credential, security, appId, timestampHeader);
ensureNonce(tenantId, appId, nonce, security);
requestBody = decryptedBody;
}
// 使用可重复读取的请求包装,供后续过滤器继续消费
CachedBodyHttpServletRequest securedRequest = new CachedBodyHttpServletRequest(request, decryptedBody);
CachedBodyHttpServletRequest securedRequest = new CachedBodyHttpServletRequest(request, requestBody);
securedRequest.setHeader(APP_ID_HEADER, credential.getAppId());
securedRequest.setHeader(HEADER_CREDENTIAL_ID, credential.getId() != null ? String.valueOf(credential.getId()) : null);
ApiGatewayAccessLogger.propagateLogIdHeader(securedRequest, accessLogId);
@@ -238,6 +241,11 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
private byte[] decryptRequestBody(byte[] originalBody,
ApiClientCredentialDO credential,
ApiGatewayProperties.Security security) {
// 检查是否启用加密,如果未启用则直接返回原文
if (credential != null && Boolean.FALSE.equals(credential.getEnableEncryption())) {
return originalBody != null ? originalBody : new byte[0];
}
if (originalBody == null || originalBody.length == 0) {
return new byte[0];
}
@@ -390,6 +398,11 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
private void encryptResponse(ContentCachingResponseWrapper responseWrapper,
ApiClientCredentialDO credential,
ApiGatewayProperties.Security security) throws IOException {
// 检查是否启用加密,如果未启用则直接返回,不加密响应
if (credential != null && Boolean.FALSE.equals(credential.getEnableEncryption())) {
return;
}
if (!security.isEncryptResponse()) {
return;
}
@@ -524,6 +537,10 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
if (security == null || credential == null) {
return false;
}
// 检查是否启用加密,如果未启用则不加密错误响应
if (Boolean.FALSE.equals(credential.getEnableEncryption())) {
return false;
}
if (!security.isEncryptResponse()) {
return false;
}

View File

@@ -46,9 +46,9 @@ public class HttpStepHandler implements ApiStepHandler {
private final WebClient.Builder webClientBuilder;
private final ExpressionExecutor expressionExecutor;
private static final Duration RETRY_DELAY = Duration.ofMillis(200);
private static final int RETRY_ATTEMPTS = 3;
private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(20);
private static final Duration RETRY_DELAY = Duration.ofSeconds(5);
private static final int RETRY_ATTEMPTS = 5;
private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(40);
private static final Set<String> DEFAULT_FORWARDED_HEADERS = Set.of(
"authorization",

View File

@@ -1,7 +1,5 @@
package com.zt.plat.module.databus.service.gateway.impl;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.common.util.object.BeanUtils;
@@ -11,14 +9,12 @@ import com.zt.plat.module.databus.dal.dataobject.gateway.ApiClientCredentialDO;
import com.zt.plat.module.databus.dal.mysql.gateway.ApiClientCredentialMapper;
import com.zt.plat.module.databus.service.gateway.ApiAnonymousUserService;
import com.zt.plat.module.databus.service.gateway.ApiClientCredentialService;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.Duration;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -36,16 +32,6 @@ public class ApiClientCredentialServiceImpl implements ApiClientCredentialServic
private final ApiClientCredentialMapper credentialMapper;
private final ApiAnonymousUserService anonymousUserService;
private LoadingCache<String, Optional<ApiClientCredentialDO>> credentialCache;
@PostConstruct
public void initCache() {
credentialCache = Caffeine.newBuilder()
.maximumSize(256)
.expireAfterWrite(Duration.ofMinutes(5))
.build(this::loadCredentialSync);
}
@Override
public PageResult<ApiClientCredentialDO> getPage(ApiClientCredentialPageReqVO reqVO) {
return credentialMapper.selectPage(reqVO);
@@ -67,7 +53,6 @@ public class ApiClientCredentialServiceImpl implements ApiClientCredentialServic
credential.setAnonymousUserId(null);
}
credentialMapper.insert(credential);
invalidateCache(credential.getAppId());
return credential.getId();
}
@@ -86,8 +71,6 @@ public class ApiClientCredentialServiceImpl implements ApiClientCredentialServic
updateObj.setAnonymousUserId(null);
}
credentialMapper.updateById(updateObj);
invalidateCache(existing.getAppId());
invalidateCache(updateObj.getAppId());
if (!Objects.equals(existing.getAnonymousUserId(), updateObj.getAnonymousUserId())) {
anonymousUserService.invalidate(existing.getAnonymousUserId());
anonymousUserService.invalidate(updateObj.getAnonymousUserId());
@@ -99,7 +82,6 @@ public class ApiClientCredentialServiceImpl implements ApiClientCredentialServic
public void delete(Long id) {
ApiClientCredentialDO existing = ensureExists(id);
credentialMapper.deleteById(id);
invalidateCache(existing.getAppId());
anonymousUserService.invalidate(existing.getAnonymousUserId());
}
@@ -118,11 +100,7 @@ public class ApiClientCredentialServiceImpl implements ApiClientCredentialServic
if (!StringUtils.hasText(appId)) {
return Optional.empty();
}
return credentialCache.get(appId.trim());
}
private Optional<ApiClientCredentialDO> loadCredentialSync(String appId) {
Optional<ApiClientCredentialDO> credential = credentialMapper.selectByAppId(appId)
Optional<ApiClientCredentialDO> credential = credentialMapper.selectByAppId(appId.trim())
.filter(item -> Boolean.TRUE.equals(item.getEnabled()));
if (credential.isEmpty()) {
log.debug("[API-PORTAL] 未找到 appId={} 的有效凭证", appId);
@@ -147,13 +125,6 @@ public class ApiClientCredentialServiceImpl implements ApiClientCredentialServic
return credential;
}
private void invalidateCache(String appId) {
if (!StringUtils.hasText(appId)) {
return;
}
credentialCache.invalidate(appId.trim());
}
private void normalizeAnonymousSettings(ApiClientCredentialSaveReqVO reqVO) {
if (Boolean.TRUE.equals(reqVO.getAllowAnonymous())) {
if (reqVO.getAnonymousUserId() == null) {