Parcourir la source

fix: OpenAPI验签根因 - ContentCachingRequestWrapper惰性缓存致requestData恒为空

根因: filter认证阶段从未消费请求体InputStream, getContentAsByteArray()返回空,
requestData恒为{} → 期望签名恒为HMAC(secret,"") → 任何带真实body的请求必然
验签失败。调用方"空串签名可以通过"即因此(空body签名恰好匹配空requestData)。

修复:
- readRequestBody先消费InputStream填充wrapper缓存, 再解析body
- 验签失败时记录诊断日志(apiKey/收到签名/期望签名/requestData), 便于联调
alphaH il y a 1 semaine
Parent
commit
6a8361a408

+ 25 - 0
java/src/main/java/com/payment/platform/core/security/TenantApiKeyAuthFilter.java

@@ -174,6 +174,16 @@ public class TenantApiKeyAuthFilter extends OncePerRequestFilter {
         //     Python: TenantApiKeyService.verify_signature(api_key_obj.api_secret, request_data, signature)
         //     → SignatureGenerator.verify_signature(api_secret, request_data, 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"
             logApiCall(keyEntity.getId(), keyEntity.getTenantId(), request, response, 401, startTime);
             writeAuthError(response, ErrorCode.AUTH_FAILED.getCode(),
@@ -253,6 +263,21 @@ public class TenantApiKeyAuthFilter extends OncePerRequestFilter {
      */
     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();
+        }
         if (content.length == 0) {
             return Map.of();
         }