Prechádzať zdrojové kódy

fix(日志): 截断后的 JSON 片段也按键脱敏

alphaH 1 týždeň pred
rodič
commit
c6e91140cc

+ 26 - 7
java/src/main/java/com/payment/platform/core/log/SensitivePayloadRedactor.java

@@ -47,11 +47,24 @@ public final class SensitivePayloadRedactor {
      * 非 JSON 路径的键名交替分支 —— 与 JSON 路径共用同一份键名集合,
      * 长键优先(避免短键在交替分支里先试而漏掉更长的名字)。
      */
+    private static final String KEY_ALTERNATION = SENSITIVE_KEYS.stream()
+            .sorted(Comparator.comparingInt(String::length).reversed())
+            .map(Pattern::quote)
+            .collect(Collectors.joining("|"));
+
+    /** form / 纯文本:{@code key=value},值到下一个 {@code &} 或空白。 */
     private static final Pattern KEY_VALUE = Pattern.compile(
-            "(?i)(" + SENSITIVE_KEYS.stream()
-                    .sorted(Comparator.comparingInt(String::length).reversed())
-                    .map(Pattern::quote)
-                    .collect(Collectors.joining("|")) + ")=([^&\\s]*)");
+            "(?i)(" + KEY_ALTERNATION + ")=([^&\\s]*)");
+
+    /**
+     * 非 JSON 兜底里的 JSON 引号键:超长 JSON 被截断成非法片段时走不到递归路径,
+     * 这里按同一份键名集合把 {@code "key":"value"}(值可含转义引号)与
+     * {@code "key":<数字/布尔/null>} 一并替换。
+     */
+    private static final Pattern JSON_QUOTED = Pattern.compile(
+            "(?i)(\"(?:" + KEY_ALTERNATION + ")\"\\s*:\\s*)"
+                    + "(?:\"(?:[^\"\\\\]|\\\\.)*\""
+                    + "|-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?|true|false|null)");
 
     private static final ObjectMapper MAPPER = new ObjectMapper();
 
@@ -72,13 +85,19 @@ public final class SensitivePayloadRedactor {
             try {
                 JsonNode node = MAPPER.readTree(truncated);
                 if (node != null && node.isContainerNode()) {
-                    return MAPPER.writeValueAsString(redactNode(node));
+                    return clamp(MAPPER.writeValueAsString(redactNode(node)));
                 }
             } catch (Exception ignored) {
-                // 截断可能把 JSON 切断;按非 JSON 的 key=value 规则兜底
+                // 截断可能把 JSON 切断;下面按 JSON 引号键与 key=value 两条规则兜底
             }
         }
-        return KEY_VALUE.matcher(truncated).replaceAll("$1=" + MASK);
+        String withoutQuoted = JSON_QUOTED.matcher(truncated).replaceAll("$1\"" + MASK + "\"");
+        return clamp(KEY_VALUE.matcher(withoutQuoted).replaceAll("$1=" + MASK));
+    }
+
+    /** 星号比原值长时(如 {@code "S1"} → {@code "***"}),最终长度仍不得超过上限。 */
+    private static String clamp(String redacted) {
+        return redacted.length() > MAX_PAYLOAD ? redacted.substring(0, MAX_PAYLOAD) : redacted;
     }
 
     /**

+ 45 - 0
java/src/test/java/com/payment/platform/core/log/SensitivePayloadRedactorTest.java

@@ -97,6 +97,51 @@ class SensitivePayloadRedactorTest {
         assertEquals(1, node.get("keep").asInt());
     }
 
+    @Test
+    void 超长JSON被截断后_app_secret仍被脱敏且不超两千() {
+        String raw = "{\"app_secret\":\"S1\",\"padding\":\"" + "x".repeat(3000) + "\"}";
+
+        String out = SensitivePayloadRedactor.redact(raw);
+
+        assertTrue(out.length() <= 2000, "实际长度 " + out.length());
+        assertFalse(out.contains("S1"), "截断后的 JSON 片段泄露了 app_secret: " + out);
+    }
+
+    @Test
+    void 超长JSON被截断后_付款码与转义的channel_extra仍被脱敏() {
+        String raw = "{\"auth_code\":\"123456\",\"channel_extra\":\"{\\\"openid\\\":\\\"o1\\\"}\","
+                + "\"padding\":\"" + "x".repeat(2500) + "\"}";
+
+        String out = SensitivePayloadRedactor.redact(raw);
+
+        assertTrue(out.length() <= 2000, "实际长度 " + out.length());
+        assertFalse(out.contains("123456"), "截断后的 JSON 片段泄露了 auth_code: " + out);
+        assertFalse(out.contains("o1"), "截断后的 JSON 片段泄露了 openid: " + out);
+        // 非敏感键保留,证明是「按键脱敏」而不是把整段丢掉
+        assertTrue(out.contains("auth_code"), out);
+    }
+
+    @Test
+    void 非JSON回退也脱敏JSON引号键的数值布尔与null() {
+        String out = SensitivePayloadRedactor.redact(
+                "前缀 \"sign\":123456,\"openid\":true,\"password\":null,\"keep\":\"ok\"");
+
+        assertTrue(out.contains("\"sign\":\"***\""), out);
+        assertTrue(out.contains("\"openid\":\"***\""), out);
+        assertTrue(out.contains("\"password\":\"***\""), out);
+        assertTrue(out.contains("\"keep\":\"ok\""), out);
+        assertFalse(out.contains("123456"), out);
+    }
+
+    @Test
+    void 非JSON回退_转义的JSON字符串值整段变星号() {
+        String out = SensitivePayloadRedactor.redact(
+                "x\"channel_extra\":\"{\\\"openid\\\":\\\"o1\\\"}\"y");
+
+        assertTrue(out.contains("\"channel_extra\":\"***\""), out);
+        assertFalse(out.contains("o1"), out);
+    }
+
     @Test
     void maskApiKey_保留前六位其余星号() {
         assertEquals("abcdef***", SensitivePayloadRedactor.maskApiKey("abcdef123456"));