需求
日志中的用户个人敏感信息需要脱敏显示
现状
项目日志中的个人敏感信息只有密码,且仅在update的语句中出现
Preparing: UPDATE user SET `modify_time` = ?, `nickname` = ?, `password` = ?, `username` = ? WHERE `id` = ? Parameters: 2025-02-25 15:34:31.599(Timestamp), xxx(String), 12345678abc(String), 123456789(String), 1(Long)
代码
Java
/**
* 自定义日志脱敏布局,用于隐藏Fluent MyBatis日志中的密码字段
* 继承Logback的LayoutBase,处理日志事件的格式化输出
*/
@Slf4j
public class PasswordMaskingLayout extends LayoutBase<ILoggingEvent> {
// 内置PatternLayout,用于保留原始日志格式(时间、线程名等)
private PatternLayout patternLayout;
// 使用ThreadLocal存储密码参数位置,确保多线程安全
private static final ThreadLocal<Integer> PASSWORD_INDEX_HOLDER = new ThreadLocal<>();
/**
* Logback组件生命周期启动方法
* 初始化PatternLayout并设置日志格式
*/
@Override
public void start() {
// 初始化 PatternLayout 并设置格式(与 logback.xml 中之前的 pattern 一致)
this.patternLayout = new PatternLayout();
this.patternLayout.setPattern("%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n");
// 共享上下文配置(关键,避免NPE)
this.patternLayout.setContext(getContext());
this.patternLayout.start(); // 关键:显式启动
super.start();
}
/**
* 生命周期停止方法,释放资源
*/
@Override
public void stop() {
this.patternLayout.stop();
super.stop();
}
/**
* 核心方法:处理日志事件的格式化输出
*/
@Override
public String doLayout(ILoggingEvent event) {
// 先通过PatternLayout生成原始格式日志
String formattedMessage = patternLayout.doLayout(event);
if (formattedMessage == null) return "";
// 根据日志内容类型处理
if (formattedMessage.contains("Preparing: ")) {
// 解析SQL语句
processPreparingMessage(formattedMessage);
} else if (formattedMessage.contains("Parameters: ")) {
// 替换密码参数
formattedMessage = processParametersMessage(formattedMessage);
}
return formattedMessage;
}
/**
* 处理SQL预处理日志,定位密码字段位置
* 示例输入:... Preparing: UPDATE user SET `password` = ? ...
*/
private void processPreparingMessage(String formattedMessage) {
// 提取SQL语句(移除前缀)
String sql = formattedMessage.replaceAll(".*Preparing: ", "");
// 解析SQL确定密码参数位置
int passwordIndex = findPasswordParameterIndex(sql);
// 存储到线程本地变量
PASSWORD_INDEX_HOLDER.set(passwordIndex);
}
/**
* 处理参数日志,替换密码值为***
* 示例输入:... Parameters: 123456(String) ...
*/
private String processParametersMessage(String formattedMessage) {
// 参数替换逻辑(与之前相同)
Integer passwordIndex = PASSWORD_INDEX_HOLDER.get();
if (passwordIndex == null || passwordIndex == -1) {
// 无需处理
return formattedMessage;
}
// 提取参数部分(移除前缀)
String paramsPart = formattedMessage.replaceAll(".*Parameters: ", "");
// 分割参数
String[] params = paramsPart.split(",\\s*");
// 替换指定位置的参数
if (passwordIndex < params.length) {
String param = params[passwordIndex];
// 保留类型信息
int typeStart = param.lastIndexOf('(');
if (typeStart != -1) {
// 如:***(String)
params[passwordIndex] = "***" + param.substring(typeStart);
} else {
// 无类型兜底
params[passwordIndex] = "***";
}
}
// 清理线程变量
PASSWORD_INDEX_HOLDER.remove();
// 重组日志消息
return formattedMessage.replace(paramsPart, String.join(", ", params));
}
/**
* 解析UPDATE语句,找到password字段的位置索引
* @param sql UPDATE语句(带SET子句)
* @return password字段的参数位置(从0开始),未找到返回-1
*/
private int findPasswordParameterIndex(String sql) {
// 正则匹配SET子句中的字段列表
Pattern updatePattern = Pattern.compile(
"UPDATE\\s+.*?\\s+SET\\s+(.*?)\\s+WHERE",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL
);
Matcher updateMatcher = updatePattern.matcher(sql);
if (updateMatcher.find()) {
// 按逗号分割字段(处理`字段名`=?, ...格式)
String setClause = updateMatcher.group(1);
String[] fields = setClause.split(",\\s*");
for (int i = 0; i < fields.length; i++) {
// 提取字段名(移除反引号/引号,分割等号左侧)
String field = fields[i].replaceAll("[`\"]", "").split("\\s*=")[0].trim();
if ("password".equalsIgnoreCase(field)) {
// 返回字段位置索引
return i;
}
}
}
// 未找到password字段
return -1;
}
}
logback配置文件
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
<property name="APP_NAME" value="hotel-channel-booking"/>
<property name="LOG_HOME" value="logs/"/>
<springProfile name="dev">
<!--控制台输出-->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="x.x.x.PasswordMaskingLayout" />
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${APP_NAME}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<FileNamePattern>${LOG_HOME}/${APP_NAME}.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<!-- 日志文件最大size -->
<maxFileSize>100MB</maxFileSize>
<!--日志文件保留天数-->
<MaxHistory>180</MaxHistory>
<!-- 日志文件合计最大size -->
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="x.x.x.PasswordMaskingLayout" />
</encoder>
</appender>
<!-- 日志输出级别 -->
<root level="debug">
<appender-ref ref="STDOUT"/>
</root>
</springProfile>
</configuration>
bug
中文字符会显示????
应该是没有设置字符集的问题,但是经查资料、乱尝试、问AI,还是不知道要在哪里配置
等有缘人解决