应通过SafeException包装器、MDC过滤器及Jackson序列化脱敏统一拦截敏感字段,禁用toString()拼接与哈希记录,全程使用不可逆占位符[REDACTED]并确保脱敏逻辑仅限日志层。

日志中打印异常时自动过滤敏感字段
Java默认的Exception.printStackTrace()或logger.error("msg", e)会把整个堆栈、变量值、甚至局部对象状态原样打出来,一旦对象里含password、idCard、token等字段,就直接泄露。
根本原因不是日志框架的问题,而是Throwable.toString()和printStackTrace()不识别业务语义——它只管反射取值,不管该不该打。
- 用
logger.error("请求失败", e)前,先调用自定义脱敏方法包装异常,不要依赖日志框架自动处理 - 对
e.getCause()和e.getSuppressed()递归脱敏,否则嵌套异常里的敏感信息照样逃逸 - 避免重写
toString()在实体类里做脱敏——这会影响所有调试、序列化、单元测试,污染面太大 - 推荐在日志门面(如SLF4J)之上加一层
SafeException包装器,只影响日志输出路径
Logback/Log4j2 中拦截并修改 MDC 和日志事件
很多团队用MDC.put("userId", user.getId())传上下文,但userId本身可能就是敏感ID(比如身份证号后6位),直接进日志模板就危险。
Logback支持Filter,Log4j2支持Layout或LoggerContext级处理器,关键不是“禁用MDC”,而是“可控地清洗”。
立即学习“Java免费学习笔记(深入)”;
- Logback:写一个继承
ch.qos.logback.core.filter.Filter的类,在decide()里调用event.getMDCPropertyMap(),对已知敏感key(如"idCard"、"phone")做replaceAll(".{3}(.{4})$", "*$1")再塞回去 - Log4j2:用
PatternLayout配合%X{userId:--}这种默认值语法没用,得靠ScriptPatternSelector或自定义LogEventFactory - 注意MDC是ThreadLocal,异步线程(如CompletableFuture)里MDC不会自动传递,脱敏逻辑若依赖MDC,必须显式
MDC.copy()后再清理
toString() 和 JSON 序列化时的隐式脱敏陷阱
你以为只控制了日志输出就安全?错。很多团队在catch块里写logger.info("req="+req),而req.toString()或JSON.toJSONString(req)会触发全量字段输出,包括被@JsonIgnore忽略的字段。
尤其Jackson默认开启SerializationFeature.WRITE_NULLS和DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES以外的几乎所有特性,@JsonInclude(NON_EMPTY)也拦不住敏感字段的非空值。
- 禁止在日志中拼接
obj.toString(),哪怕你给实体写了“安全版”toString()——开发容易忘,Code Review难覆盖 - 用Jackson时,全局配置
SimpleModule注册BeanSerializerModifier,对标注了@Sensitive注解的字段统一替换为"***" - Lombok的
@ToString(exclude = "password")只管Lombok生成的toString(),不影响Jackson、Hibernate、甚至IDE调试视图,别高估它的作用范围
异常堆栈中还原原始参数值的常见误操作
有人想“既然不能打原始值,那就打个哈希”——比如logger.error("token="+DigestUtils.md5Hex(token))。这反而更危险:攻击者可构造碰撞哈希反推原文,或利用日志中的哈希+已知算法批量爆破。
还有人用Arrays.toString(charArray)打密码数组,殊不知char[]转字符串后,GC前仍驻留堆内存,堆转储(heap dump)里一抓一个准。
- 日志中任何涉及敏感数据的操作,目标不是“变形”,而是“不可逆地擦除”——最稳妥是打固定占位符
"[REDACTED]",且确保该字符串不在业务逻辑中被误用 - 不要依赖
String::strip()或substring()来“隐藏”,它们只是创建新字符串,旧字符串还在堆里,直到GC - 如果必须记录标识性信息(如脱敏后的手机号
138****1234),确保该逻辑只在日志适配层执行,且与业务代码完全隔离,防止误当真实值使用
真正难的不是写脱敏逻辑,而是让所有logger.xxx()调用点都经过同一套清洗管道——中间只要漏掉一个System.out.println()、一个IDE调试断点、一次临时加的日志,整条链路就失效。










