|
@@ -174,6 +174,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(),
|
|
@@ -253,6 +263,21 @@ public class TenantApiKeyAuthFilter extends OncePerRequestFilter {
|
|
|
*/
|
|
*/
|
|
|
private Map<String, Object> readRequestBody(ContentCachingRequestWrapper request) {
|
|
private Map<String, Object> readRequestBody(ContentCachingRequestWrapper request) {
|
|
|
byte[] content = request.getContentAsByteArray();
|
|
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) {
|
|
if (content.length == 0) {
|
|
|
return Map.of();
|
|
return Map.of();
|
|
|
}
|
|
}
|