1. 限制记录 api 日志的字段长度
2. 完整记录所有的 databus api 的请求日志 3. 新增 iwork 同步可以按 id 维度进行 4. 新增自动扫描 BusinessBaseDO 的 公司部门数据权限模式
This commit is contained in:
@@ -3,9 +3,12 @@ package com.zt.plat.module.databus.framework.integration.gateway.core;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.zt.plat.framework.common.util.monitor.TracerUtils;
|
||||
import com.zt.plat.framework.common.util.servlet.ServletUtils;
|
||||
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiAccessLogDO;
|
||||
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
|
||||
import com.zt.plat.module.databus.framework.integration.gateway.security.CachedBodyHttpServletRequest;
|
||||
import com.zt.plat.module.databus.service.gateway.ApiAccessLogService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -20,8 +23,7 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 将 API 调用上下文持久化为访问日志。
|
||||
@@ -33,6 +35,8 @@ public class ApiGatewayAccessLogger {
|
||||
|
||||
public static final String ATTR_LOG_ID = "ApiAccessLogId";
|
||||
public static final String ATTR_EXCEPTION_STACK = "ApiAccessLogExceptionStack";
|
||||
public static final String HEADER_ACCESS_LOG_ID = "X-Databus-AccessLog-Id";
|
||||
private static final String ATTR_REQUEST_START = "ApiAccessLogRequestStart";
|
||||
|
||||
private static final int MAX_TEXT_LENGTH = 4000;
|
||||
|
||||
@@ -44,24 +48,15 @@ public class ApiGatewayAccessLogger {
|
||||
*/
|
||||
public void onRequest(ApiInvocationContext context) {
|
||||
try {
|
||||
String traceId = TracerUtils.getTraceId();
|
||||
ApiAccessLogDO logDO = new ApiAccessLogDO();
|
||||
logDO.setTraceId(traceId);
|
||||
logDO.setApiCode(context.getApiCode());
|
||||
logDO.setApiVersion(context.getApiVersion());
|
||||
logDO.setRequestMethod(context.getHttpMethod());
|
||||
logDO.setRequestPath(context.getRequestPath());
|
||||
logDO.setRequestQuery(toJson(context.getRequestQueryParams()));
|
||||
logDO.setRequestHeaders(toJson(context.getRequestHeaders()));
|
||||
logDO.setRequestBody(toJson(context.getRequestBody()));
|
||||
logDO.setClientIp(firstNonBlank(context.getClientIp(),
|
||||
GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), "X-Forwarded-For")));
|
||||
logDO.setUserAgent(GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), HttpHeaders.USER_AGENT));
|
||||
logDO.setStatus(3); // 默认未知
|
||||
logDO.setRequestTime(toLocalDateTime(context.getRequestTime()));
|
||||
logDO.setTenantId(parseTenantId(context.getTenantId()));
|
||||
Long logId = apiAccessLogService.create(logDO);
|
||||
context.getAttributes().put(ATTR_LOG_ID, logId);
|
||||
ApiAccessLogDO logDO = buildRequestSnapshot(context);
|
||||
Long existingLogId = getLogId(context);
|
||||
if (existingLogId != null) {
|
||||
logDO.setId(existingLogId);
|
||||
apiAccessLogService.update(logDO);
|
||||
} else {
|
||||
Long logId = apiAccessLogService.create(logDO);
|
||||
context.getAttributes().put(ATTR_LOG_ID, logId);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.warn("记录 API 访问日志开始阶段失败, traceId={}", TracerUtils.getTraceId(), ex);
|
||||
}
|
||||
@@ -111,15 +106,97 @@ public class ApiGatewayAccessLogger {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全过滤阶段的第一时间记录请求元数据,保证被快速拒绝的请求也能查询。
|
||||
*/
|
||||
public Long logEntrance(HttpServletRequest request) {
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
Object existing = request.getAttribute(ATTR_LOG_ID);
|
||||
if (existing instanceof Long logId) {
|
||||
return logId;
|
||||
}
|
||||
try {
|
||||
ApiAccessLogDO logDO = new ApiAccessLogDO();
|
||||
logDO.setTraceId(TracerUtils.getTraceId());
|
||||
logDO.setRequestMethod(request.getMethod());
|
||||
logDO.setRequestPath(request.getRequestURI());
|
||||
logDO.setRequestQuery(truncate(request.getQueryString()));
|
||||
logDO.setRequestHeaders(toJson(collectHeaders(request)));
|
||||
logDO.setClientIp(ServletUtils.getClientIP(request));
|
||||
logDO.setUserAgent(request.getHeader(HttpHeaders.USER_AGENT));
|
||||
logDO.setStatus(3);
|
||||
logDO.setRequestTime(LocalDateTime.now());
|
||||
Long logId = apiAccessLogService.create(logDO);
|
||||
request.setAttribute(ATTR_LOG_ID, logId);
|
||||
request.setAttribute(ATTR_REQUEST_START, Instant.now());
|
||||
return logId;
|
||||
} catch (Exception ex) {
|
||||
log.warn("记录入口 API 访问日志失败", ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编排前即结束的请求在此补写状态码、耗时等关键信息。
|
||||
*/
|
||||
public void finalizeEarly(HttpServletRequest request, int status, String message) {
|
||||
if (request == null) {
|
||||
return;
|
||||
}
|
||||
Object existing = request.getAttribute(ATTR_LOG_ID);
|
||||
if (!(existing instanceof Long logId)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ApiAccessLogDO update = new ApiAccessLogDO();
|
||||
update.setId(logId);
|
||||
update.setResponseStatus(status);
|
||||
update.setResponseMessage(truncate(message));
|
||||
update.setStatus(resolveStatus(status));
|
||||
update.setResponseTime(LocalDateTime.now());
|
||||
update.setDuration(calculateDuration(request));
|
||||
apiAccessLogService.update(update);
|
||||
} catch (Exception ex) {
|
||||
log.warn("更新入口 API 访问日志失败, logId={}", logId, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将入口阶段生成的 logId 通过请求头继续传递,供后续流程关联合并。
|
||||
*/
|
||||
public static void propagateLogIdHeader(CachedBodyHttpServletRequest requestWrapper, Long logId) {
|
||||
if (requestWrapper == null || logId == null) {
|
||||
return;
|
||||
}
|
||||
requestWrapper.setHeader(HEADER_ACCESS_LOG_ID, String.valueOf(logId));
|
||||
}
|
||||
|
||||
private Long getLogId(ApiInvocationContext context) {
|
||||
Object value = context.getAttributes().get(ATTR_LOG_ID);
|
||||
if (value instanceof Long) {
|
||||
return (Long) value;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
return null;
|
||||
return value instanceof Long ? (Long) value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据编排上下文构建请求侧快照,用于访问日志首段信息。
|
||||
*/
|
||||
private ApiAccessLogDO buildRequestSnapshot(ApiInvocationContext context) {
|
||||
ApiAccessLogDO logDO = new ApiAccessLogDO();
|
||||
logDO.setTraceId(TracerUtils.getTraceId());
|
||||
logDO.setApiCode(context.getApiCode());
|
||||
logDO.setApiVersion(context.getApiVersion());
|
||||
logDO.setRequestMethod(context.getHttpMethod());
|
||||
logDO.setRequestPath(context.getRequestPath());
|
||||
logDO.setRequestQuery(toJson(context.getRequestQueryParams()));
|
||||
logDO.setRequestHeaders(toJson(context.getRequestHeaders()));
|
||||
logDO.setRequestBody(toJson(context.getRequestBody()));
|
||||
logDO.setClientIp(context.getClientIp());
|
||||
logDO.setUserAgent(GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), HttpHeaders.USER_AGENT));
|
||||
logDO.setStatus(3);
|
||||
logDO.setRequestTime(toLocalDateTime(context.getRequestTime()));
|
||||
logDO.setTenantId(parseTenantId(context.getTenantId()));
|
||||
return logDO;
|
||||
}
|
||||
|
||||
private Long calculateDuration(ApiInvocationContext context) {
|
||||
@@ -130,6 +207,14 @@ public class ApiGatewayAccessLogger {
|
||||
return Duration.between(start, Instant.now()).toMillis();
|
||||
}
|
||||
|
||||
private Long calculateDuration(HttpServletRequest request) {
|
||||
Object startAttr = request.getAttribute(ATTR_REQUEST_START);
|
||||
if (startAttr instanceof Instant start) {
|
||||
return Duration.between(start, Instant.now()).toMillis();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Integer resolveStatus(Integer httpStatus) {
|
||||
if (httpStatus == null) {
|
||||
return 3;
|
||||
@@ -214,6 +299,19 @@ public class ApiGatewayAccessLogger {
|
||||
return extra;
|
||||
}
|
||||
|
||||
@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 toJson(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
@@ -265,27 +363,20 @@ public class ApiGatewayAccessLogger {
|
||||
}
|
||||
}
|
||||
|
||||
private String firstNonBlank(String... values) {
|
||||
if (values == null) {
|
||||
return null;
|
||||
private Map<String, String> collectHeaders(HttpServletRequest request) {
|
||||
if (request == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
for (String value : values) {
|
||||
if (StringUtils.hasText(value)) {
|
||||
return value;
|
||||
Map<String, String> headers = new LinkedHashMap<>();
|
||||
Enumeration<String> names = request.getHeaderNames();
|
||||
while (names != null && names.hasMoreElements()) {
|
||||
String name = names.nextElement();
|
||||
Enumeration<String> values = request.getHeaders(name);
|
||||
if (values == null || !values.hasMoreElements()) {
|
||||
continue;
|
||||
}
|
||||
headers.put(name, values.nextElement());
|
||||
}
|
||||
return null;
|
||||
return headers;
|
||||
}
|
||||
|
||||
private Object firstNonNull(Object... values) {
|
||||
if (values == null) {
|
||||
return null;
|
||||
}
|
||||
for (Object value : values) {
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.zt.plat.module.databus.framework.integration.gateway.core;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties;
|
||||
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
|
||||
import com.zt.plat.module.databus.framework.integration.gateway.core.ApiGatewayAccessLogger;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -90,6 +91,7 @@ public class ApiGatewayRequestMapper {
|
||||
});
|
||||
context.setUserAgent(GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), HttpHeaders.USER_AGENT));
|
||||
context.setClientIp(resolveClientIp(headers, context.getRequestHeaders()));
|
||||
captureAccessLogId(context);
|
||||
populateQueryParams(headers, context, originalRequestUri);
|
||||
if (properties.isEnableTenantHeader()) {
|
||||
Object tenantHeaderValue = context.getRequestHeaders().get(properties.getTenantHeader());
|
||||
@@ -114,6 +116,21 @@ public class ApiGatewayRequestMapper {
|
||||
return context;
|
||||
}
|
||||
|
||||
private void captureAccessLogId(ApiInvocationContext context) {
|
||||
String headerValue = GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), ApiGatewayAccessLogger.HEADER_ACCESS_LOG_ID);
|
||||
if (!StringUtils.hasText(headerValue)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Long logId = Long.valueOf(headerValue);
|
||||
context.getAttributes().put(ApiGatewayAccessLogger.ATTR_LOG_ID, logId);
|
||||
} catch (NumberFormatException ex) {
|
||||
// 忽略格式问题,仅在属性中保留原文以便排查
|
||||
context.getAttributes().put(ApiGatewayAccessLogger.ATTR_LOG_ID, headerValue);
|
||||
}
|
||||
context.getRequestHeaders().remove(ApiGatewayAccessLogger.HEADER_ACCESS_LOG_ID);
|
||||
}
|
||||
|
||||
private boolean isInternalHeader(String headerName) {
|
||||
if (!StringUtils.hasText(headerName)) {
|
||||
return true;
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.zt.plat.framework.tenant.core.context.TenantContextHolder;
|
||||
import com.zt.plat.framework.web.core.util.WebFrameworkUtils;
|
||||
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiClientCredentialDO;
|
||||
import com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties;
|
||||
import com.zt.plat.module.databus.framework.integration.gateway.core.ApiGatewayAccessLogger;
|
||||
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiGatewayResponse;
|
||||
import com.zt.plat.module.databus.service.gateway.ApiAnonymousUserService;
|
||||
import com.zt.plat.module.databus.service.gateway.ApiClientCredentialService;
|
||||
@@ -56,6 +57,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
|
||||
private final ApiClientCredentialService credentialService;
|
||||
private final ApiAnonymousUserService anonymousUserService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ApiGatewayAccessLogger accessLogger;
|
||||
private final AntPathMatcher pathMatcher = new AntPathMatcher();
|
||||
private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {};
|
||||
|
||||
@@ -72,18 +74,24 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
Long accessLogId = accessLogger.logEntrance(request);
|
||||
// 校验访问 IP 是否落在允许范围内
|
||||
if (!isIpAllowed(request)) {
|
||||
log.warn("[API-PORTAL] 拦截来自 IP {} 访问 {} 的请求", request.getRemoteAddr(), pathWithinApplication);
|
||||
response.sendError(HttpStatus.FORBIDDEN.value(), "IP 禁止访问");
|
||||
accessLogger.finalizeEarly(request, HttpStatus.FORBIDDEN.value(), "IP 禁止访问");
|
||||
return;
|
||||
}
|
||||
ApiGatewayProperties.Security security = properties.getSecurity();
|
||||
ApiClientCredentialDO credential = null;
|
||||
if (!security.isEnabled()) {
|
||||
filterChain.doFilter(request, response);
|
||||
byte[] originalBody = StreamUtils.copyToByteArray(request.getInputStream());
|
||||
CachedBodyHttpServletRequest passthroughRequest = new CachedBodyHttpServletRequest(request, originalBody);
|
||||
ApiGatewayAccessLogger.propagateLogIdHeader(passthroughRequest, accessLogId);
|
||||
filterChain.doFilter(passthroughRequest, response);
|
||||
return;
|
||||
}
|
||||
boolean dispatchedToGateway = false;
|
||||
try {
|
||||
Long tenantId = resolveTenantId(request);
|
||||
// 从请求头解析 appId 并加载客户端凭证,包含匿名访问配置
|
||||
@@ -118,6 +126,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
|
||||
|
||||
// 使用可重复读取的请求包装,供后续过滤器继续消费
|
||||
CachedBodyHttpServletRequest securedRequest = new CachedBodyHttpServletRequest(request, decryptedBody);
|
||||
ApiGatewayAccessLogger.propagateLogIdHeader(securedRequest, accessLogId);
|
||||
if (StringUtils.hasText(request.getCharacterEncoding())) {
|
||||
securedRequest.setCharacterEncoding(request.getCharacterEncoding());
|
||||
}
|
||||
@@ -129,6 +138,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
|
||||
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
|
||||
try {
|
||||
filterChain.doFilter(securedRequest, responseWrapper);
|
||||
dispatchedToGateway = true;
|
||||
encryptResponse(responseWrapper, credential, security);
|
||||
} finally {
|
||||
responseWrapper.copyBodyToResponse();
|
||||
@@ -136,9 +146,15 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
|
||||
} catch (SecurityValidationException ex) {
|
||||
log.warn("[API-PORTAL] 安全校验失败: {}", ex.getMessage());
|
||||
writeErrorResponse(response, security, credential, ex.status(), ex.getMessage());
|
||||
if (!dispatchedToGateway) {
|
||||
accessLogger.finalizeEarly(request, ex.status().value(), ex.getMessage());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.error("[API-PORTAL] 处理安全校验时出现异常", ex);
|
||||
writeErrorResponse(response, security, credential, HttpStatus.INTERNAL_SERVER_ERROR, "网关安全校验失败");
|
||||
if (!dispatchedToGateway) {
|
||||
accessLogger.finalizeEarly(request, HttpStatus.INTERNAL_SERVER_ERROR.value(), "网关安全校验失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user