task-190: 日志脱敏契约(StructuredLog.maskToken 统一 Token 部分脱敏,ImageVideo maskForLog 委托;URL query/异常消息体不入日志、防注入)+ 8 条测试
This commit is contained in:
@@ -54,6 +54,18 @@ public final class StructuredLog {
|
|||||||
return result.isEmpty() ? MISSING : result;
|
return result.isEmpty() ? MISSING : result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 统一 Token 部分脱敏:≤10 位全打码,长 token 保留前6+后4,中间 ***(与既有 maskForLog 一致)。 */
|
||||||
|
public static String maskToken(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String text = value.trim();
|
||||||
|
if (text.length() <= 10) {
|
||||||
|
return "***";
|
||||||
|
}
|
||||||
|
return text.substring(0, 6) + "***" + text.substring(text.length() - 4);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按规范字段集渲染 key=value 串:仅输出规范字段、顺序固定,缺失字段用占位符,
|
* 按规范字段集渲染 key=value 串:仅输出规范字段、顺序固定,缺失字段用占位符,
|
||||||
* 避免不同日志字段漂移。
|
* 避免不同日志字段漂移。
|
||||||
|
|||||||
+2
-8
@@ -787,13 +787,7 @@ public class ImageVideoCozeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String maskForLog(String value) {
|
private String maskForLog(String value) {
|
||||||
if (!hasText(value)) {
|
// 统一走 StructuredLog.maskToken(task-190:Token 部分脱敏规则单一来源)
|
||||||
return "";
|
return com.nanri.aiimage.config.StructuredLog.maskToken(value);
|
||||||
}
|
|
||||||
String text = value.trim();
|
|
||||||
if (text.length() <= 10) {
|
|
||||||
return "***";
|
|
||||||
}
|
|
||||||
return text.substring(0, 6) + "***" + text.substring(text.length() - 4);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package com.nanri.aiimage.config;
|
||||||
|
|
||||||
|
import ch.qos.logback.classic.Logger;
|
||||||
|
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||||
|
import ch.qos.logback.core.read.ListAppender;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* task-190:日志脱敏契约(spec 11 §2)。
|
||||||
|
* Token 部分脱敏规则单一来源(StructuredLog.maskToken,ImageVideo 已委托);结构化字段值
|
||||||
|
* 消毒防日志注入;重试守卫的 URL(去 query)/异常消息体不入日志,保证敏感原文不落盘。
|
||||||
|
*/
|
||||||
|
class LogRedactionTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shortTokenFullyMasked() {
|
||||||
|
assertEquals("***", StructuredLog.maskToken("abc123"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void longTokenPartiallyMaskedNoOriginal() {
|
||||||
|
String original = "abcdefghij-ABCDEFGHIJ-secret-token-000";
|
||||||
|
String masked = StructuredLog.maskToken(original);
|
||||||
|
assertEquals(original.substring(0, 6) + "***" + original.substring(original.length() - 4), masked);
|
||||||
|
assertFalse(masked.contains(original), "脱敏结果不应包含原始 token");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nullBlankTokenMaskedEmpty() {
|
||||||
|
assertEquals("", StructuredLog.maskToken(null));
|
||||||
|
assertEquals("", StructuredLog.maskToken(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void maskingDeterministic() {
|
||||||
|
String token = "abcdefghijk-1234567890";
|
||||||
|
assertEquals(StructuredLog.maskToken(token), StructuredLog.maskToken(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void structuredFieldStripsNewlineNoLogInjection() {
|
||||||
|
String value = StructuredLog.field("正常前缀\r\nInject: stolen; more");
|
||||||
|
assertFalse(value.contains("\r"), "字段值不应含回车");
|
||||||
|
assertFalse(value.contains("\n"), "字段值不应含换行,防止日志注入");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void guardStripsTokenFromUrlQueryInLog() throws Exception {
|
||||||
|
IdempotentRetryGuard guard = new IdempotentRetryGuard(fastProps());
|
||||||
|
ListAppender<ILoggingEvent> captured = attach();
|
||||||
|
try {
|
||||||
|
AtomicInteger n = new AtomicInteger();
|
||||||
|
guard.execute("POST", true, "brand", "http://example/x?token=SECRET-QUERY-999", () -> {
|
||||||
|
if (n.incrementAndGet() < 2) {
|
||||||
|
throw new IOException("连接中断");
|
||||||
|
}
|
||||||
|
return "ok";
|
||||||
|
});
|
||||||
|
String text = join(captured);
|
||||||
|
assertFalse(text.contains("SECRET-QUERY-999"), "URL query 中的 token 不应入日志: " + text);
|
||||||
|
} finally {
|
||||||
|
detach(captured);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void guardExceptionMessageNotLogged() throws Exception {
|
||||||
|
IdempotentRetryGuard guard = new IdempotentRetryGuard(fastProps());
|
||||||
|
ListAppender<ILoggingEvent> captured = attach();
|
||||||
|
try {
|
||||||
|
AtomicInteger n = new AtomicInteger();
|
||||||
|
guard.execute("POST", true, "brand", "http://example/x", () -> {
|
||||||
|
if (n.incrementAndGet() < 2) {
|
||||||
|
throw new IOException("响应: Bearer secret-token-abc-9999");
|
||||||
|
}
|
||||||
|
return "ok";
|
||||||
|
});
|
||||||
|
String text = join(captured);
|
||||||
|
assertFalse(text.contains("secret-token-abc-9999"), "异常消息体不应入日志: " + text);
|
||||||
|
assertFalse(text.contains("Bearer"), "鉴权字样不应入日志");
|
||||||
|
} finally {
|
||||||
|
detach(captured);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void normalFieldsKeptWhileSensitiveMasked() {
|
||||||
|
// 普通字段原样保留;敏感字段走 maskToken 后不再含原文
|
||||||
|
assertTrue(StructuredLog.format(Map.of("taskId", 7L)).contains("taskId=7"), "普通字段应保留");
|
||||||
|
String token = "normal-token-value-1234567890";
|
||||||
|
assertFalse(StructuredLog.maskToken(token).equals(token), "maskToken 不应原样返回 token");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpClientProperties fastProps() {
|
||||||
|
HttpClientProperties p = new HttpClientProperties();
|
||||||
|
p.setBaseRetryDelayMillis(1);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ListAppender<ILoggingEvent> attach() {
|
||||||
|
Logger guardLogger = (Logger) LoggerFactory.getLogger(IdempotentRetryGuard.class);
|
||||||
|
ListAppender<ILoggingEvent> appender = new ListAppender<>();
|
||||||
|
appender.start();
|
||||||
|
guardLogger.addAppender(appender);
|
||||||
|
return appender;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void detach(ListAppender<ILoggingEvent> appender) {
|
||||||
|
((Logger) LoggerFactory.getLogger(IdempotentRetryGuard.class)).detachAppender(appender);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String join(ListAppender<ILoggingEvent> appender) {
|
||||||
|
return String.join("\n", appender.list.stream().map(ILoggingEvent::getFormattedMessage).toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user