logback自定义日志过滤器

需求

日志中的用户个人敏感信息需要脱敏显示

现状

项目日志中的个人敏感信息只有密码,且仅在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,还是不知道要在哪里配置

等有缘人解决

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

糟糕捏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值