修复数据总线访问日志无法显示状态码问题: http://172.16.46.63:31560/index.php?m=task&f=view&taskID=703. databus 新增 client 统一出口内容管理审计: http://172.16.46.63:31560/index.php?m=task&f=view&taskID=716
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -61,6 +61,7 @@ package-lock.json
|
||||
# visual studio code
|
||||
.history
|
||||
*.log
|
||||
logs/**
|
||||
|
||||
functions/mock
|
||||
.temp/**
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
<jsoup.version>1.18.1</jsoup.version>
|
||||
<lombok.version>1.18.36</lombok.version>
|
||||
<mapstruct.version>1.6.3</mapstruct.version>
|
||||
<hutool-5.version>5.8.35</hutool-5.version>
|
||||
<hutool-5.version>5.8.43</hutool-5.version>
|
||||
<hutool-6.version>6.0.0-M19</hutool-6.version>
|
||||
<easyexcel.version>4.0.3</easyexcel.version>
|
||||
<velocity.version>2.4.1</velocity.version>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<module>zt-module-databus-api</module>
|
||||
<module>zt-module-databus-server</module>
|
||||
<module>zt-module-databus-server-app</module>
|
||||
<module>zt-module-databus-client</module>
|
||||
</modules>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.zt.plat.module.databus.api.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.zt.plat.framework.common.util.json.databind.TimestampLocalDateTimeSerializer;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 新增Databus API 访问日志请求体。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode
|
||||
public class ApiAccessLogCreateReq {
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@Schema(description = "主键")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* HTTP 方法
|
||||
*/
|
||||
@Schema(description = "HTTP 方法")
|
||||
@NotNull(message = "HTTP 方法不能为空")
|
||||
private String requestMethod;
|
||||
|
||||
/**
|
||||
* 请求路径
|
||||
*/
|
||||
@Schema(description = "请求路径")
|
||||
@NotNull(message = "请求路径不能为空")
|
||||
private String requestPath;
|
||||
|
||||
/**
|
||||
* 调用使用的应用标识
|
||||
*/
|
||||
@Schema(description = "调用使用的应用标识")
|
||||
@NotNull(message = "调用使用的应用标识不能为空")
|
||||
private String credentialAppId;
|
||||
|
||||
/**
|
||||
* 多租户编号
|
||||
*/
|
||||
@Schema(description = "多租户编号")
|
||||
@NotNull(message = "多租户编号不能为空")
|
||||
private Long tenantId;
|
||||
|
||||
/**
|
||||
* 查询参数(JSON 字符串)
|
||||
*/
|
||||
@Schema(description = "查询参数(JSON 字符串)")
|
||||
private String requestQuery;
|
||||
|
||||
/**
|
||||
* 请求头信息(JSON 字符串)
|
||||
*/
|
||||
@Schema(description = "请求头信息(JSON 字符串)")
|
||||
private String requestHeaders;
|
||||
|
||||
/**
|
||||
* 请求体(JSON 字符串)
|
||||
*/
|
||||
@Schema(description = "请求体(JSON 字符串)")
|
||||
private String requestBody;
|
||||
|
||||
/**
|
||||
* 响应 HTTP 状态码
|
||||
*/
|
||||
@Schema(description = "响应 HTTP 状态码")
|
||||
private Integer responseStatus;
|
||||
|
||||
/**
|
||||
* 响应提示信息
|
||||
*/
|
||||
@Schema(description = "响应提示信息")
|
||||
private String responseMessage;
|
||||
|
||||
/**
|
||||
* 响应体(JSON 字符串)
|
||||
*/
|
||||
@Schema(description = "响应体(JSON 字符串)")
|
||||
private String responseBody;
|
||||
|
||||
/**
|
||||
* 访问状态:0-成功 1-客户端错误 2-服务端错误 3-未知
|
||||
*/
|
||||
@Schema(description = "访问状态:0-成功 1-客户端错误 2-服务端错误 3-未知")
|
||||
@NotNull(message = "访问状态不能为空")
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 业务错误码
|
||||
*/
|
||||
@Schema(description = "业务错误码")
|
||||
private String errorCode;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
@Schema(description = "错误信息")
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 异常堆栈
|
||||
*/
|
||||
@Schema(description = "异常堆栈")
|
||||
private String exceptionStack;
|
||||
|
||||
/**
|
||||
* 客户端 IP
|
||||
*/
|
||||
@Schema(description = "客户端 IP")
|
||||
private String clientIp;
|
||||
|
||||
/**
|
||||
* User-Agent
|
||||
*/
|
||||
@Schema(description = "User-Agent")
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 请求耗时(毫秒)
|
||||
*/
|
||||
@Schema(description = "请求耗时(毫秒)")
|
||||
private Long duration;
|
||||
|
||||
/**
|
||||
* 请求时间
|
||||
*/
|
||||
@Schema(description = "请求时间")
|
||||
@JsonSerialize(using = TimestampLocalDateTimeSerializer.class)
|
||||
private LocalDateTime requestTime;
|
||||
|
||||
/**
|
||||
* 响应时间
|
||||
*/
|
||||
@Schema(description = "响应时间")
|
||||
@JsonSerialize(using = TimestampLocalDateTimeSerializer.class)
|
||||
private LocalDateTime responseTime;
|
||||
|
||||
/**
|
||||
* 额外调试信息(JSON 字符串)
|
||||
*/
|
||||
@Schema(description = "额外调试信息(JSON 字符串)")
|
||||
private String extra;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.zt.plat.module.databus.api.provider;
|
||||
|
||||
import com.zt.plat.framework.common.pojo.CommonResult;
|
||||
import com.zt.plat.module.databus.api.dto.ApiAccessLogCreateReq;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Databus API访问日志接口
|
||||
* 2026/1/20 16:26
|
||||
*/
|
||||
@FeignClient(name = "${databus.provider.log.service:databus-server}")
|
||||
@Tag(name = "RPC 服务 - Databus API访问日志接口")
|
||||
public interface DatabusAccessLogProviderApi {
|
||||
|
||||
String PREFIX = "/databus/api/portal/access-log";
|
||||
|
||||
@PostMapping(PREFIX + "/add")
|
||||
@Operation(summary = "新增访问日志")
|
||||
CommonResult<Boolean> add(@RequestHeader Map<String, String> headers, @RequestBody ApiAccessLogCreateReq req);
|
||||
|
||||
}
|
||||
82
zt-module-databus/zt-module-databus-client/pom.xml
Normal file
82
zt-module-databus/zt-module-databus-client/pom.xml
Normal file
@@ -0,0 +1,82 @@
|
||||
<?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-client</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>${project.artifactId}</name>
|
||||
<description>
|
||||
Databus client, 提供调用第三方服务的能力并记录调用日志。
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-module-databus-api</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,242 @@
|
||||
package com.zt.plat.module.databus.client;
|
||||
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.hutool.http.Method;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.zt.plat.framework.common.pojo.CommonResult;
|
||||
import com.zt.plat.framework.common.util.security.CryptoSignatureUtils;
|
||||
import com.zt.plat.module.databus.api.dto.ApiAccessLogCreateReq;
|
||||
import com.zt.plat.module.databus.api.provider.DatabusAccessLogProviderApi;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 数据总线提供的 http 客户端, 通过此客户端发起接口调用,会自动记录请求日志到数据总线
|
||||
* 2026/1/20 09:44
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class DatabusClient {
|
||||
|
||||
/**
|
||||
* 多租户编号
|
||||
*/
|
||||
@Value("${zt.plat.databus.client.tenantId:1}")
|
||||
private Long tenantId;
|
||||
|
||||
@Resource
|
||||
private DatabusAccessLogProviderApi databusAccessLogProviderApi;
|
||||
|
||||
private static final int MAX_TEXT_LENGTH = 4000;
|
||||
|
||||
/**
|
||||
* 发送 get 请求
|
||||
* @param urlString 仅接口地址,不带参数,参数由data提供
|
||||
* @param data 请求参数
|
||||
* @param headers 请求头
|
||||
* @return 响应结果
|
||||
*/
|
||||
public String get(String urlString, Map<String, Object> data, Map<String, String> headers, String appId, String authToken) {
|
||||
return doRequest(urlString, data, headers, Method.GET, appId, authToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 post 请求
|
||||
* @param urlString 仅接口地址,不带参数,参数由data提供
|
||||
* @param data 请求数据
|
||||
* @param headers 请求头
|
||||
* @return 响应结果
|
||||
*/
|
||||
public String post(String urlString, Map<String, Object> data, Map<String, String> headers, String appId, String authToken) {
|
||||
return doRequest(urlString, data, headers, Method.POST, appId, authToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 put 请求
|
||||
* @param urlString 仅接口地址,不带参数,参数由data提供
|
||||
* @param data 请求数据
|
||||
* @param headers 请求头
|
||||
* @return 响应结果
|
||||
*/
|
||||
public String put(String urlString, Map<String, Object> data, Map<String, String> headers, String appId, String authToken) {
|
||||
return doRequest(urlString, data, headers, Method.PUT, appId, authToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 delete 请求
|
||||
* @param urlString 仅接口地址,不带参数,参数由data提供
|
||||
* @param data 请求数据
|
||||
* @param headers 请求头
|
||||
* @return 响应结果
|
||||
*/
|
||||
public String delete(String urlString, Map<String, Object> data, Map<String, String> headers, String appId, String authToken) {
|
||||
return doRequest(urlString, data, headers, Method.DELETE, appId, authToken);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 发送请求到门户(token模式,不证书加密)
|
||||
* @param urlString 仅接口地址,不带参数,参数由data提供
|
||||
* @param data 请求数据
|
||||
* @param headers 请求头
|
||||
* @param method 请求方式,默认为 GET
|
||||
* @return 响应结果
|
||||
*/
|
||||
public String doRequest(String urlString, Map<String, Object> data, Map<String, String> headers, Method method, String appId, String authToken) {
|
||||
if (method == null) {
|
||||
method = Method.GET;
|
||||
}
|
||||
Assert.hasText(urlString, "接口地址不能为空");
|
||||
HttpRequest request;
|
||||
ApiAccessLogCreateReq logReq = new ApiAccessLogCreateReq();
|
||||
if (Method.GET.equals(method) || Method.DELETE.equals(method)) {
|
||||
logReq.setRequestQuery(JSONUtil.toJsonStr(data));
|
||||
} else {
|
||||
logReq.setRequestBody(JSONUtil.toJsonStr(data));
|
||||
}
|
||||
request = HttpUtil.createRequest(method, urlString).form(data);
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
request.header(entry.getKey(), entry.getValue(), true);
|
||||
}
|
||||
}
|
||||
|
||||
logReq.setTenantId(tenantId);
|
||||
|
||||
logReq.setRequestMethod(method.name());
|
||||
logReq.setRequestPath(urlString);
|
||||
logReq.setCredentialAppId(appId);
|
||||
logReq.setRequestHeaders(JSONUtil.toJsonStr(headers));
|
||||
logReq.setUserAgent(request.header("User-Agent"));
|
||||
String result;
|
||||
logReq.setRequestTime(LocalDateTime.now());
|
||||
long requestTime = System.currentTimeMillis();
|
||||
try (HttpResponse response = request.execute()) {
|
||||
logReq.setDuration(System.currentTimeMillis() - requestTime);
|
||||
logReq.setResponseTime(LocalDateTime.now());
|
||||
result = response.body();
|
||||
logReq.setResponseStatus(response.getStatus());
|
||||
logReq.setResponseBody(result);
|
||||
logReq.setStatus(resolveStatus(response.getStatus()));
|
||||
Map<String, String> errorCodeAndMsg = extractErrorCodeAndMsg(result, response.getStatus());
|
||||
logReq.setErrorCode(errorCodeAndMsg.get("errorCode"));
|
||||
logReq.setErrorMessage(errorCodeAndMsg.get("errorMessage"));
|
||||
addAccessLog(logReq, appId, authToken);
|
||||
} catch (Exception e) {
|
||||
// 错误的日志服务端记录了,这里就不再记录了
|
||||
// logReq.setStatus(1);
|
||||
// logReq.setExceptionStack(buildStackTrace( e));
|
||||
// addAccessLog(logReq, appId, authToken);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void addAccessLog(ApiAccessLogCreateReq logReq, String appId, String authToken) {
|
||||
String nonce = randomNonce();
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("tenant-id", String.valueOf(tenantId));
|
||||
headers.put("ZT-App-Id", appId);
|
||||
headers.put("ZT-Timestamp", Long.toString(System.currentTimeMillis()));
|
||||
headers.put("ZT-Nonce", nonce);
|
||||
headers.put("ZT-Auth-Token", authToken);
|
||||
CommonResult<Boolean> response = databusAccessLogProviderApi.add(headers, logReq);
|
||||
if (response.getCode() != 0) {
|
||||
throw new RuntimeException("添加访问日志失败: " + response);
|
||||
}
|
||||
}
|
||||
|
||||
private static String randomNonce() {
|
||||
return UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
|
||||
private String buildStackTrace(Throwable throwable) {
|
||||
try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
|
||||
throwable.printStackTrace(pw);
|
||||
return truncate(sw.toString());
|
||||
} catch (Exception ex) {
|
||||
return throwable.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private Integer resolveStatus(Integer httpStatus) {
|
||||
if (httpStatus == null) {
|
||||
return 3;
|
||||
}
|
||||
if (httpStatus >= 200 && httpStatus < 400) {
|
||||
return 0;
|
||||
}
|
||||
if (httpStatus >= 400 && httpStatus < 500) {
|
||||
return 1;
|
||||
}
|
||||
if (httpStatus >= 500) {
|
||||
return 2;
|
||||
}
|
||||
return 3;
|
||||
}
|
||||
|
||||
private Map<String, String> extractErrorCodeAndMsg(String responseBody, Integer responseStatus) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
if (!isErrorStatus(responseStatus)) {
|
||||
return result;
|
||||
}
|
||||
if (JSONUtil.isTypeJSONObject(responseBody)) {
|
||||
JSONObject map = JSONUtil.parseObj(responseBody);
|
||||
Object errorCode = firstNonNull(map.get("errorCode"), map.get("code"));
|
||||
errorCode = errorCode == null ? null : truncate(String.valueOf(errorCode));
|
||||
if (errorCode != null) {
|
||||
result.put("errorCode", errorCode.toString());
|
||||
}
|
||||
Object message = firstNonNull(map.get("errorMessage"), map.get("message"));
|
||||
if (message != null) {
|
||||
message = truncate(String.valueOf(message));
|
||||
result.put("errorMessage", message.toString());
|
||||
}
|
||||
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private boolean isErrorStatus(Integer responseStatus) {
|
||||
return responseStatus != null && responseStatus >= 400;
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
private <T> T firstNonNull(T... candidates) {
|
||||
if (candidates == null) {
|
||||
return null;
|
||||
}
|
||||
for (T candidate : candidates) {
|
||||
if (candidate != null) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String truncate(String text) {
|
||||
if (!StringUtils.hasText(text)) {
|
||||
return text;
|
||||
}
|
||||
if (text.length() <= MAX_TEXT_LENGTH) {
|
||||
return text;
|
||||
}
|
||||
return text.substring(0, MAX_TEXT_LENGTH);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.zt.plat.module.databus.client;
|
||||
|
||||
/**
|
||||
*
|
||||
* 2026/1/21 10:48
|
||||
*/
|
||||
|
||||
import com.zt.plat.module.databus.api.provider.DatabusAccessLogProviderApi;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableFeignClients(clients = {DatabusAccessLogProviderApi.class})
|
||||
public class RpcConfiguration {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
com.zt.plat.module.databus.client.DatabusClient,\
|
||||
com.zt.plat.module.databus.client.RpcConfiguration
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.zt.plat.module.databus;
|
||||
|
||||
import com.zt.plat.module.databus.client.DatabusClient;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/**
|
||||
*
|
||||
* 2026/1/20 14:29
|
||||
*/
|
||||
@SpringBootTest(classes = TestApplication.class)
|
||||
public class DatabusClientTest {
|
||||
|
||||
@Autowired
|
||||
private DatabusClient databusClient;
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
String result = databusClient.get("https://www.baidu.com/", null, null, "jwyw2", "a5d7cf609c0b47038ea405c660726ee9");
|
||||
System.out.println(result);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.zt.plat.module.databus;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/**
|
||||
*
|
||||
* 2026/1/20 14:26
|
||||
*/
|
||||
@SpringBootTest
|
||||
@SpringBootApplication(exclude = {
|
||||
DataSourceAutoConfiguration.class,
|
||||
DataSourceTransactionManagerAutoConfiguration.class,
|
||||
HibernateJpaAutoConfiguration.class,
|
||||
JdbcTemplateAutoConfiguration.class,
|
||||
})
|
||||
public class TestApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(TestApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
server-addr: 172.16.46.63:30848 # Nacos 服务器地址
|
||||
username: nacos # Nacos 账号
|
||||
password: P@ssword25 # Nacos 密码
|
||||
discovery: # 【配置中心】配置项
|
||||
namespace: klw # 命名空间。这里使用 maven Profile 资源过滤进行动态替换
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
metadata:
|
||||
version: 1.0.0 # 服务实例的版本号,可用于灰度发布
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.zt.plat.module.databus.api;
|
||||
|
||||
import com.zt.plat.framework.common.pojo.CommonResult;
|
||||
import com.zt.plat.framework.common.util.monitor.TracerUtils;
|
||||
import com.zt.plat.framework.common.util.object.BeanUtils;
|
||||
import com.zt.plat.framework.common.util.servlet.ServletUtils;
|
||||
import com.zt.plat.module.databus.api.dto.ApiAccessLogCreateReq;
|
||||
import com.zt.plat.module.databus.api.provider.DatabusAccessLogProviderApi;
|
||||
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiAccessLogDO;
|
||||
import com.zt.plat.module.databus.service.gateway.ApiAccessLogService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.zt.plat.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
*
|
||||
* 2026/1/21 10:05
|
||||
*/
|
||||
@RestController
|
||||
public class DatabusAccessLogProviderApiImpl implements DatabusAccessLogProviderApi {
|
||||
|
||||
@Resource
|
||||
private ApiAccessLogService apiAccessLogService;
|
||||
|
||||
@Override
|
||||
@PermitAll
|
||||
public CommonResult<Boolean> add(Map<String, String> headers, ApiAccessLogCreateReq req) {
|
||||
ApiAccessLogDO logDO = new ApiAccessLogDO();
|
||||
BeanUtils.copyProperties(req, logDO);
|
||||
logDO.setTraceId(TracerUtils.getTraceId());
|
||||
logDO.setClientIp(ServletUtils.getClientIP());
|
||||
logDO.setCreateTime(LocalDateTime.now());
|
||||
logDO.setUpdateTime(LocalDateTime.now());
|
||||
logDO.setCreator("1");
|
||||
logDO.setUpdater("1");
|
||||
|
||||
apiAccessLogService.create(logDO);
|
||||
return success(Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
@@ -159,6 +159,7 @@ public class ApiGatewayAccessLogger {
|
||||
update.setStatus(resolveStatus(status));
|
||||
update.setResponseTime(LocalDateTime.now());
|
||||
update.setDuration(calculateDuration(request));
|
||||
update.setUpdater("1");
|
||||
apiAccessLogService.update(update);
|
||||
} catch (Exception ex) {
|
||||
log.warn("更新入口 API 访问日志失败, logId={}", logId, ex);
|
||||
|
||||
@@ -89,6 +89,7 @@ public class ApiGatewayExecutionService {
|
||||
} else {
|
||||
responseContext = apiFlowDispatcher.dispatch(context.getApiCode(), context.getApiVersion(), context);
|
||||
}
|
||||
responseContext.setResponseStatus(resolveStatus(responseContext));
|
||||
} catch (ServiceException ex) {
|
||||
errorProcessor.applyServiceException(context, ex);
|
||||
accessLogger.onException(context, ex);
|
||||
|
||||
@@ -77,6 +77,9 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加日志接口需要特殊处理
|
||||
boolean isNormalProcess = !pathMatcher.match("/databus/api/portal/access-log/**", pathWithinApplication);
|
||||
Long accessLogId = null;
|
||||
ApiGatewayProperties.Security security = properties.getSecurity();
|
||||
ApiClientCredentialDO credential = null;
|
||||
@@ -95,8 +98,10 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
|
||||
accessLogger.finalizeEarly(request, HttpStatus.FORBIDDEN.value(), "IP 禁止访问");
|
||||
return;
|
||||
}
|
||||
// IP 校验通过后再补录入口日志,避免无租户信息写库
|
||||
// IP 校验通过后再补录入口日志,避免无租户信息写库, 非日志添加接口才记录日志
|
||||
if (isNormalProcess) {
|
||||
accessLogId = accessLogger.logEntrance(request);
|
||||
}
|
||||
if (!security.isEnabled()) {
|
||||
byte[] originalBody = StreamUtils.copyToByteArray(request.getInputStream());
|
||||
CachedBodyHttpServletRequest passthroughRequest = new CachedBodyHttpServletRequest(request, originalBody);
|
||||
@@ -128,14 +133,18 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
|
||||
if (nonce.length() < 8) {
|
||||
throw new SecurityValidationException(HttpStatus.BAD_REQUEST, "随机数长度不足");
|
||||
}
|
||||
String signature = requireHeader(request, SIGNATURE_HEADER, "缺少签名");
|
||||
|
||||
if (isNormalProcess) {
|
||||
// 非日志添加接口才处理
|
||||
String signature = requireHeader(request, SIGNATURE_HEADER, "缺少签名");
|
||||
// 尝试按凭证配置解密请求体,并构建签名载荷进行校验
|
||||
byte[] decryptedBody = decryptRequestBody(requestBody, credential, security);
|
||||
verifySignature(request, decryptedBody, signature, credential, security, appId, timestampHeader);
|
||||
ensureNonce(tenantId, appId, nonce, security);
|
||||
requestBody = decryptedBody;
|
||||
}
|
||||
ensureNonce(tenantId, appId, nonce, security);
|
||||
|
||||
}
|
||||
|
||||
// 使用可重复读取的请求包装,供后续过滤器继续消费
|
||||
CachedBodyHttpServletRequest securedRequest = new CachedBodyHttpServletRequest(request, requestBody);
|
||||
@@ -154,7 +163,9 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
|
||||
try {
|
||||
filterChain.doFilter(securedRequest, responseWrapper);
|
||||
dispatchedToGateway = true;
|
||||
if (isNormalProcess) {
|
||||
encryptResponse(responseWrapper, credential, security);
|
||||
}
|
||||
} finally {
|
||||
responseWrapper.copyBodyToResponse();
|
||||
}
|
||||
@@ -163,7 +174,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
|
||||
accessLogId = accessLogger.logEntrance(request);
|
||||
}
|
||||
log.warn("[API-PORTAL] 安全校验失败: {}", ex.getMessage());
|
||||
writeErrorResponse(response, security, credential, ex.status(), ex.getMessage());
|
||||
writeErrorResponse(response, security, credential, ex.status(), ex.getMessage(), isNormalProcess);
|
||||
if (!dispatchedToGateway) {
|
||||
accessLogger.finalizeEarly(request, ex.status().value(), ex.getMessage());
|
||||
}
|
||||
@@ -172,7 +183,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
|
||||
accessLogId = accessLogger.logEntrance(request);
|
||||
}
|
||||
log.error("[API-PORTAL] 处理安全校验时出现异常", ex);
|
||||
writeErrorResponse(response, security, credential, HttpStatus.INTERNAL_SERVER_ERROR, "网关安全校验失败");
|
||||
writeErrorResponse(response, security, credential, HttpStatus.INTERNAL_SERVER_ERROR, "网关安全校验失败", isNormalProcess);
|
||||
if (!dispatchedToGateway) {
|
||||
accessLogger.finalizeEarly(request, HttpStatus.INTERNAL_SERVER_ERROR.value(), "网关安全校验失败");
|
||||
}
|
||||
@@ -479,6 +490,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
|
||||
String token = tokenOptional.get();
|
||||
securedRequest.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
|
||||
securedRequest.setHeader(GatewayJwtResolver.HEADER_ZT_AUTH_TOKEN, token);
|
||||
|
||||
}
|
||||
|
||||
private static final class SecurityValidationException extends RuntimeException {
|
||||
@@ -499,7 +511,8 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
|
||||
ApiGatewayProperties.Security security,
|
||||
ApiClientCredentialDO credential,
|
||||
HttpStatus status,
|
||||
String message) {
|
||||
String message,
|
||||
boolean isNormalProcess) {
|
||||
if (response.isCommitted()) {
|
||||
log.warn("[API-PORTAL] 响应已提交,无法写入安全校验错误: {}", message);
|
||||
return;
|
||||
@@ -514,7 +527,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
|
||||
.response(null)
|
||||
.traceId(traceId)
|
||||
.build();
|
||||
if (shouldEncryptErrorResponse(security, credential)) {
|
||||
if (shouldEncryptErrorResponse(security, credential) && isNormalProcess) {
|
||||
String encryptionKey = credential.getEncryptionKey();
|
||||
String encryptionType = resolveEncryptionType(credential, security);
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user