java日志脱敏核心是隐藏异常堆栈中的敏感参数而非屏蔽堆栈本身,需通过safeexception包装器递归清洗异常链、禁用tostring/json拼接式日志、mdc上下文过滤、stackwalker裁剪堆栈等多层防护。

Java中日志脱敏在打印异常堆栈时隐藏敏感参数,核心不是“不让堆栈出现”,而是“让堆栈里不带敏感值”。默认的 logger.error("msg", e) 会完整输出异常对象及其所有字段(包括嵌套对象里的 password、token、idCard 等),而 Throwable 的 toString() 和 printStackTrace() 机制本身不识别业务语义,只做反射取值——这才是泄露根源。
用 SafeException 包装器统一拦截异常链
不要依赖日志框架自动处理,也不能在 catch 块里直接写 log.error("", e)。应在抛出或记录前,用自定义包装器递归清洗整个异常链:
- 对
e.getCause()和e.getSuppressed()逐层调用脱敏逻辑,防止嵌套异常逃逸 - 包装器应保留原始堆栈(
fillInStackTrace()不重置),但替换 message 中的敏感内容(如把"password=123456"替为"password=[REDACTED]") - 禁止在实体类中重写
toString()做脱敏——这会影响调试、序列化和测试,污染面过大
禁用 toString() 和 JSON 拼接式日志
很多“看起来安全”的写法实际危险:
-
log.info("req=" + req)→ 触发req.toString(),哪怕用了 Lombok@ToString(exclude="pwd"),也只对 toString 有效,对 JSON 序列化无效 -
JSON.toJSONString(req)会绕过 Jackson 的@JsonIgnore,输出所有字段(包括被忽略的敏感字段) - 正确做法:日志中只传明确字段,如
log.info("login fail, user={}", maskUserId(user.getId()))
在 MDC 和日志事件层面过滤上下文
MDC.put("userId", "110101199003072XXX") 这类操作本身就有风险,不能只靠日志 pattern 控制输出:
- Logback:写一个继承
ch.qos.logback.core.filter.Filter的类,在decide()中遍历event.getMDCPropertyMap(),对 key 为"phone"、"idCard"的值做掩码(如replaceAll(".{3}(.{4}).*", "*$1")) - Log4j2:避免依赖
%X{userId:--}这类简单语法,改用ScriptPatternSelector或自定义LogEventFactory在事件创建时清洗 - 注意:CompletableFuture 等异步场景下,MDC 不自动传递,需显式
MDC.copy()后再脱敏
StackWalker 配合轻量级堆栈裁剪
Java 9+ 可用 StackWalker 替代 getStackTrace(),精准控制哪些帧进日志:
- 启用
Option.RETAIN_CLASS_REFERENCE后,用filter(f -> f.getClassName().startsWith("com.yourbiz"))只留业务帧,跳过 spring、cglib、java.* 等干扰项 - 结合
skip(2)跳过日志拦截器、全局处理器等顶层无关帧 - 过滤含本地路径的帧(如
/tmp/、/home/),防止泄露部署环境细节
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











