|
|
@@ -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.TenantApiLogMapper;
|
|
|
import jakarta.servlet.FilterChain;
|
|
|
+import jakarta.servlet.ReadListener;
|
|
|
import jakarta.servlet.ServletException;
|
|
|
+import jakarta.servlet.ServletInputStream;
|
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
|
+import jakarta.servlet.http.HttpServletRequestWrapper;
|
|
|
import jakarta.servlet.http.HttpServletResponse;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
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.stereotype.Component;
|
|
|
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.InputStreamReader;
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
import java.time.OffsetDateTime;
|
|
|
import java.util.List;
|
|
|
@@ -123,7 +127,7 @@ public class TenantApiKeyAuthFilter extends OncePerRequestFilter {
|
|
|
long startTime = System.currentTimeMillis();
|
|
|
|
|
|
// 包装 request 以便可以多次读取 body (Python L87-90 中读了一次 JSON body)
|
|
|
- ContentCachingRequestWrapper wrappedRequest = wrapRequest(request);
|
|
|
+ CachedBodyRequestWrapper wrappedRequest = wrapRequest(request);
|
|
|
|
|
|
// ---- 1. 提取 Authorization header (Python L37-41) ----
|
|
|
String apiKey = extractApiKey(wrappedRequest);
|
|
|
@@ -261,23 +265,8 @@ public class TenantApiKeyAuthFilter extends OncePerRequestFilter {
|
|
|
* 读取请求体 JSON — 对应 Python L87-90
|
|
|
* request_data = await request.json()
|
|
|
*/
|
|
|
- private Map<String, Object> readRequestBody(ContentCachingRequestWrapper request) {
|
|
|
- byte[] content = request.getContentAsByteArray();
|
|
|
- if (content.length == 0) {
|
|
|
- // ContentCachingRequestWrapper 惰性缓存: getContentAsByteArray() 只在 InputStream
|
|
|
- // 被消费后才填充。认证阶段无人读过 body (Controller 的 @RequestBody 在 filter 之后),
|
|
|
- // 必须主动消费一次, 否则 requestData 恒为空 Map → 期望签名恒为 HMAC(secret, ""),
|
|
|
- // 任何带真实请求体的签名必然验签失败 (调用方"空串签名可以通过"的根因)。
|
|
|
- // 消费后 Controller 的 @RequestBody 仍可正常读取 (wrapper 缓存 body 支持多次读)。
|
|
|
- try {
|
|
|
- jakarta.servlet.ServletInputStream in = request.getInputStream();
|
|
|
- byte[] buf = new byte[4096];
|
|
|
- while (in.read(buf) != -1) { /* 消费以填充缓存 */ }
|
|
|
- } catch (IOException e) {
|
|
|
- log.debug("读取请求体失败: {}", e.getMessage());
|
|
|
- }
|
|
|
- content = request.getContentAsByteArray();
|
|
|
- }
|
|
|
+ private Map<String, Object> readRequestBody(CachedBodyRequestWrapper request) {
|
|
|
+ byte[] content = request.getCachedBody();
|
|
|
if (content.length == 0) {
|
|
|
return Map.of();
|
|
|
}
|
|
|
@@ -368,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);
|
|
|
}
|
|
|
}
|