|
@@ -11,8 +11,11 @@ import com.payment.platform.module.payment.apikey.entity.TenantApiLogEntity;
|
|
|
import com.payment.platform.module.payment.apikey.mapper.TenantApiKeyMapper;
|
|
import com.payment.platform.module.payment.apikey.mapper.TenantApiKeyMapper;
|
|
|
import com.payment.platform.module.payment.apikey.mapper.TenantApiLogMapper;
|
|
import com.payment.platform.module.payment.apikey.mapper.TenantApiLogMapper;
|
|
|
import jakarta.servlet.FilterChain;
|
|
import jakarta.servlet.FilterChain;
|
|
|
|
|
+import jakarta.servlet.ReadListener;
|
|
|
import jakarta.servlet.ServletException;
|
|
import jakarta.servlet.ServletException;
|
|
|
|
|
+import jakarta.servlet.ServletInputStream;
|
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
|
|
|
+import jakarta.servlet.http.HttpServletRequestWrapper;
|
|
|
import jakarta.servlet.http.HttpServletResponse;
|
|
import jakarta.servlet.http.HttpServletResponse;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.RequiredArgsConstructor;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
@@ -23,9 +26,10 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
|
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
|
|
import org.springframework.stereotype.Component;
|
|
import org.springframework.stereotype.Component;
|
|
|
import org.springframework.web.filter.OncePerRequestFilter;
|
|
import org.springframework.web.filter.OncePerRequestFilter;
|
|
|
-import org.springframework.web.util.ContentCachingRequestWrapper;
|
|
|
|
|
-
|
|
|
|
|
|
|
+import java.io.BufferedReader;
|
|
|
|
|
+import java.io.ByteArrayInputStream;
|
|
|
import java.io.IOException;
|
|
import java.io.IOException;
|
|
|
|
|
+import java.io.InputStreamReader;
|
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.charset.StandardCharsets;
|
|
|
import java.time.OffsetDateTime;
|
|
import java.time.OffsetDateTime;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
@@ -123,7 +127,7 @@ public class TenantApiKeyAuthFilter extends OncePerRequestFilter {
|
|
|
long startTime = System.currentTimeMillis();
|
|
long startTime = System.currentTimeMillis();
|
|
|
|
|
|
|
|
// 包装 request 以便可以多次读取 body (Python L87-90 中读了一次 JSON body)
|
|
// 包装 request 以便可以多次读取 body (Python L87-90 中读了一次 JSON body)
|
|
|
- ContentCachingRequestWrapper wrappedRequest = wrapRequest(request);
|
|
|
|
|
|
|
+ CachedBodyRequestWrapper wrappedRequest = wrapRequest(request);
|
|
|
|
|
|
|
|
// ---- 1. 提取 Authorization header (Python L37-41) ----
|
|
// ---- 1. 提取 Authorization header (Python L37-41) ----
|
|
|
String apiKey = extractApiKey(wrappedRequest);
|
|
String apiKey = extractApiKey(wrappedRequest);
|
|
@@ -174,6 +178,16 @@ public class TenantApiKeyAuthFilter extends OncePerRequestFilter {
|
|
|
// Python: TenantApiKeyService.verify_signature(api_key_obj.api_secret, request_data, signature)
|
|
// Python: TenantApiKeyService.verify_signature(api_key_obj.api_secret, request_data, signature)
|
|
|
// → SignatureGenerator.verify_signature(api_secret, request_data, signature)
|
|
// → SignatureGenerator.verify_signature(api_secret, request_data, signature)
|
|
|
if (!SignatureGenerator.verifySignature(keyEntity.getApiSecret(), requestData, signature)) {
|
|
if (!SignatureGenerator.verifySignature(keyEntity.getApiSecret(), requestData, signature)) {
|
|
|
|
|
+ // 诊断日志: 记录后端实际收到的请求体与两端签名, 用于联调定位 (不记录 apiSecret)
|
|
|
|
|
+ try {
|
|
|
|
|
+ log.error("OpenAPI签名验证失败: apiKey={}, tenantId={}, 收到Signature={}, 期望签名={}, requestData={}",
|
|
|
|
|
+ apiKey, keyEntity.getTenantId(), signature,
|
|
|
|
|
+ SignatureGenerator.generateSignature(keyEntity.getApiSecret(), requestData),
|
|
|
|
|
+ objectMapper.writeValueAsString(requestData));
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("OpenAPI签名验证失败: apiKey={}, 收到Signature={} (记录请求体失败: {})",
|
|
|
|
|
+ apiKey, signature, e.getMessage());
|
|
|
|
|
+ }
|
|
|
// Python L106: "Invalid Signature"
|
|
// Python L106: "Invalid Signature"
|
|
|
logApiCall(keyEntity.getId(), keyEntity.getTenantId(), request, response, 401, startTime);
|
|
logApiCall(keyEntity.getId(), keyEntity.getTenantId(), request, response, 401, startTime);
|
|
|
writeAuthError(response, ErrorCode.AUTH_FAILED.getCode(),
|
|
writeAuthError(response, ErrorCode.AUTH_FAILED.getCode(),
|
|
@@ -251,8 +265,8 @@ public class TenantApiKeyAuthFilter extends OncePerRequestFilter {
|
|
|
* 读取请求体 JSON — 对应 Python L87-90
|
|
* 读取请求体 JSON — 对应 Python L87-90
|
|
|
* request_data = await request.json()
|
|
* request_data = await request.json()
|
|
|
*/
|
|
*/
|
|
|
- private Map<String, Object> readRequestBody(ContentCachingRequestWrapper request) {
|
|
|
|
|
- byte[] content = request.getContentAsByteArray();
|
|
|
|
|
|
|
+ private Map<String, Object> readRequestBody(CachedBodyRequestWrapper request) {
|
|
|
|
|
+ byte[] content = request.getCachedBody();
|
|
|
if (content.length == 0) {
|
|
if (content.length == 0) {
|
|
|
return Map.of();
|
|
return Map.of();
|
|
|
}
|
|
}
|
|
@@ -343,10 +357,77 @@ public class TenantApiKeyAuthFilter extends OncePerRequestFilter {
|
|
|
|
|
|
|
|
// ========================= 工具 =========================
|
|
// ========================= 工具 =========================
|
|
|
|
|
|
|
|
- private ContentCachingRequestWrapper wrapRequest(HttpServletRequest request) {
|
|
|
|
|
- if (request instanceof ContentCachingRequestWrapper) {
|
|
|
|
|
- return (ContentCachingRequestWrapper) request;
|
|
|
|
|
|
|
+ private CachedBodyRequestWrapper wrapRequest(HttpServletRequest request) {
|
|
|
|
|
+ if (request instanceof CachedBodyRequestWrapper) {
|
|
|
|
|
+ return (CachedBodyRequestWrapper) request;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ return new CachedBodyRequestWrapper(request);
|
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
|
+ // 读取 body 失败(连接中断等): 记录日志, 返回无 body 的包装(验签会失败, 由后续逻辑处理)
|
|
|
|
|
+ log.warn("缓存请求体失败: {}", e.getMessage());
|
|
|
|
|
+ return new CachedBodyRequestWrapper(request, new byte[0]);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ========================= 请求体缓存 wrapper (支持多次读取) =========================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 构造时一次性读入请求体, getInputStream()/getReader() 基于缓存的 byte[] 返回新流,
|
|
|
|
|
+ * 因此 filter 验签消费后, Controller 的 @RequestBody 仍可正常读取。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 不能使用 ContentCachingRequestWrapper: 其 getInputStream() 只返回同一个流,
|
|
|
|
|
+ * 被消费后即 EOF, Controller 再读会得到空 body → HttpMessageNotReadableException
|
|
|
|
|
+ * "Required request body is missing"。
|
|
|
|
|
+ */
|
|
|
|
|
+ static class CachedBodyRequestWrapper extends HttpServletRequestWrapper {
|
|
|
|
|
+ private final byte[] body;
|
|
|
|
|
+
|
|
|
|
|
+ CachedBodyRequestWrapper(HttpServletRequest request) throws IOException {
|
|
|
|
|
+ super(request);
|
|
|
|
|
+ try (ServletInputStream is = request.getInputStream()) {
|
|
|
|
|
+ this.body = is.readAllBytes();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ CachedBodyRequestWrapper(HttpServletRequest request, byte[] body) {
|
|
|
|
|
+ super(request);
|
|
|
|
|
+ this.body = body;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ byte[] getCachedBody() {
|
|
|
|
|
+ return body;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public ServletInputStream getInputStream() {
|
|
|
|
|
+ ByteArrayInputStream bais = new ByteArrayInputStream(body);
|
|
|
|
|
+ return new ServletInputStream() {
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public int read() {
|
|
|
|
|
+ return bais.read();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public boolean isFinished() {
|
|
|
|
|
+ return bais.available() == 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public boolean isReady() {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void setReadListener(ReadListener listener) {
|
|
|
|
|
+ // 非异步读取, 无需回调
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public BufferedReader getReader() {
|
|
|
|
|
+ return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
|
|
|
}
|
|
}
|
|
|
- return new ContentCachingRequestWrapper(request);
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|