实现原理

本文实现日志脱敏,是借鉴了logback中自带的PatternLayoutEncoder类,重写了其start方法,在此方法中使用了我们自己的MyLogbackPatternLayout类创建格式化输出对象,MyLogbackPatternLayout类的doLayout方法中实现了正则替换的处理逻辑,可结合代码加断点测试以便更好了解具体过程

代码实现

定义RegexReplacement

package my.logback;

import java.util.regex.Pattern;

public class RegexReplacement {
    /**
     * 脱敏匹配正则
     */
    private Pattern regex;
    /**
     * 替换正则
     */
    private String replacement;
    /**
     * Perform the replacement.
     *
     * @param msg The String to match against.
     * @return the replacement String.
     */
    public String format(final String msg) {
        return regex.matcher(msg).replaceAll(replacement);
    }

    public Pattern getRegex() {
        return regex;
    }

    public void setRegex(String regex) {
        this.regex = Pattern.compile(regex);
    }

    public String getReplacement() {
        return replacement;
    }

    public void setReplacement(String replacement) {
        this.replacement = replacement;
    }
}

定义MyLogbackReplaces

package my.logback;

import java.util.ArrayList;
import java.util.List;

public class MyLogbackReplaces {
    /**
     * 脱敏正则列表
     */
    private List<RegexReplacement> replace = new ArrayList<>();
    /**
     * 添加规则(因为replace类型是list,必须指定addReplace方法用以添加多个)
     *
     * @param replacement replacement
     */
    public void addReplace(RegexReplacement replacement) {
        replace.add(replacement);
    }

    public List<RegexReplacement> getReplace() {
        return replace;
    }

    public void setReplace(List<RegexReplacement> replace) {
        this.replace = replace;
    }
}

定义MyLogbackPatternLayout

package my.logback;

import ch.qos.logback.classic.PatternLayout;
import ch.qos.logback.classic.spi.ILoggingEvent;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.status.StatusLogger;

public class MyLogbackPatternLayout extends PatternLayout {
    /**
     * logger
     */
    private static final Logger LOGGER = StatusLogger.getLogger();
    /**
     * 正则替换规则
     */
    private MyLogbackReplaces replaces;
    /**
     * 是否开启脱敏,默认关闭(false)
     */
    private Boolean sensitive;

    public MyLogbackPatternLayout(MyLogbackReplaces replaces, Boolean sensitive) {
        super();
        this.replaces = replaces;
        this.sensitive = sensitive;
    }

    /**
     * 格式化日志信息
     *
     * @param event ILoggingEvent
     * @return 日志信息
     */
    @Override
    public String doLayout(ILoggingEvent event) {
        // 占位符填充
        String msg = super.doLayout(event);
        // 脱敏处理
        return this.buildSensitiveMsg(msg);
    }

    /**
     * 根据配置对日志进行脱敏
     *
     * @param msg 消息
     * @return 脱敏后的日志信息
     */
    public String buildSensitiveMsg(String msg) {
        if (sensitive == null || !sensitive) {
            // 未开启脱敏
            return msg;
        }
        if (this.replaces == null || this.replaces.getReplace() == null || this.replaces.getReplace().isEmpty()) {
            LOGGER.error("日志脱敏开启,但未配置脱敏规则,请检查配置后重试");
            return msg;
        }

        String sensitiveMsg = msg;

        for (RegexReplacement replace : this.replaces.getReplace()) {
            // 遍历脱敏正则 & 替换敏感数据
            sensitiveMsg = replace.format(sensitiveMsg);
        }
        return sensitiveMsg;
    }
}

定义MyLogbackPatternLayoutEncoder

package my.logback;

import ch.qos.logback.classic.encoder.PatternLayoutEncoder;

public class MyLogbackPatternLayoutEncoder extends PatternLayoutEncoder {
    /**
     * 正则替换规则
     */
    private MyLogbackReplaces replaces;
    /**
     * 是否开启脱敏,默认关闭(false)
     */
    private Boolean sensitive = false;

    /**
     * 使用自定义TbspLogbackPatternLayout格式化输出
     */
    @Override
    public void start() {
        MyLogbackPatternLayout patternLayout = new MyLogbackPatternLayout(replaces, sensitive);
        patternLayout.setContext(context);
        patternLayout.setPattern(this.getPattern());
        patternLayout.setOutputPatternAsHeader(outputPatternAsHeader);
        patternLayout.start();
        this.layout = patternLayout;
        started = true;
    }

    public boolean isSensitive() {
        return sensitive;
    }

    public void setSensitive(boolean sensitive) {
        this.sensitive = sensitive;
    }

    public MyLogbackReplaces getReplaces() {
        return replaces;
    }

    public void setReplaces(MyLogbackReplaces replaces) {
        this.replaces = replaces;
    }
}

配置文件

logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--please pay attention that: file name should not be logback.xml,
name it logback-spring.xml to use it in springboot framework-->
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 指定为自己写的PatternLayoutEncoder -->
        <encoder class="my.logback.MyLogbackPatternLayoutEncoder">
            <pattern>%d{HH:mm:ss.SSS} %-5level %logger{80} --- %msg%n</pattern>
            <!-- 日志字符集(默认ISO-8859-1) -->
            <charset>UTF-8</charset>
            <!-- 开启脱敏(默认false) -->
            <sensitive>true</sensitive>
            <!-- 脱敏规则列表 -->
            <replaces>
                <!-- 脱敏规则 -->
                <replace>
                    <!-- 11位的手机号:保留前3后4 -->
                    <regex>
                        <![CDATA[
				(mobile|手机号)(=|=\[|\":\"|:|:|='|':')(1)([3-9]{2})(\d{4})(\d{4})(\]|\"|'|)
							]]>
                    </regex>
                    <replacement>$1$2$3$4****$6$7</replacement>
                </replace>
                <replace>
                    <!-- 固定电话: XXXX-XXXXXXXX或XXX-XXXXXXXX,保留区号+前2后2 -->
                    <regex>
                        <![CDATA[
				(tel|座机)(=|=\[|\":\"|:|:|='|':')([\d]{3,4}-)(\d{2})(\d{4})(\d{2})(\]|\"|'|)
							]]>
                    </regex>
                    <replacement>$1$2$3$4****$6$7</replacement>
                </replace>

                <replace>
                    <!-- 地址:汉字+字母+数字+下划线+中划线,留前3个汉字 -->
                    <regex>
                        <![CDATA[
				(地址|住址|address)(=|=\[|\":\"|:|:|='|':')([\u4e00-\u9fa5]{3})(\w|[\u4e00-\u9fa5]|-)*(\]|\"|'|)
							]]>
                    </regex>
                    <replacement>$1$2$3****$5</replacement>
                </replace>


                <replace>
                    <!-- 19位的卡号,保留后4 -->
                    <regex>
                        <![CDATA[
				(cardNo|卡号)(=|=\[|\":\"|:|:|='|':')(\d{15})(\d{4})(\]|\"|'|)
							]]>
                    </regex>
                    <replacement>$1$2***************$4$5</replacement>
                </replace>

                <replace>
                    <!-- 姓名,2-4汉字,留前1-->
                    <regex>
                        <![CDATA[
				(name|姓名)(=|=\[|\":\"|:|:|='|':')([\u4e00-\u9fa5]{1})([\u4e00-\u9fa5]{1,3})(\]|\"|'|)
							]]>
                    </regex>
                    <replacement>$1$2$3**$5</replacement>
                </replace>

                <replace>
                    <!--  密码 6位数字,全* -->
                    <regex>
                        <![CDATA[
					(password|密码|验证码)(=|=\[|\":\"|:|:|='|':')(\d{6})(\]|\"|'|)
							]]>
                    </regex>
                    <replacement>$1$2******$4</replacement>
                </replace>

                <replace>
                    <!-- 身份证,18位(结尾为数字或X、x),保留前1后1 -->
                    <regex>
                        <![CDATA[
							(身份证号|idCard)(=|=\[|\":\"|:|:|='|':')(\d{1})(\d{16})([\d|X|x]{1})(\]|\"|)
							]]>
                    </regex>
                    <replacement>$1$2$3****************$5$6</replacement>
                </replace>

                <replace>
                    <!-- 邮箱,保留@前的前1后1 -->
                    <regex>
                        <![CDATA[
							(\w{1})(\w*)(\w{1})@(\w+).com
							]]>
                    </regex>
                    <replacement>$1****$3@$4.com</replacement>
                </replace>
            </replaces>

        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="STDOUT"/>
    </root>

</configuration>

测试

class Job {
    /**
     * jobName
     */
    private String jobName;
    /**
     * salary
     */
    private int salary;
    /**
     * company
     */
    private String company;
    /**
     * address
     */
    private String address;
    /**
     * tel
     */
    private String tel;
    /**
     * position
     */
    private List<String> position;

	// getter, setter, toString等省略
}

class User {
    /**
     * name
     */
    private String name;
    /**
     * idCard
     */
    private String idCard;
    /**
     * cardNo
     */
    private String cardNo;
    /**
     * mobile
     */
    private String mobile;
    /**
     * tel
     */
    private String tel;
    /**
     * password
     */
    private String password;
    /**
     * email
     */
    private String email;
    /**
     * address
     */
    private String address;
    /**
     * birth
     */
    private Date birth;
    /**
     * job
     */
    private Job job;
	// getter, setter, toString等省略
}

public class LogSensitiveTest {
    private static final Logger logger = LoggerFactory.getLogger(LogSensitiveTest.class);

    @Test
    public void test0() {
        // 等号
        logger.infoMessage("mobile={}", "13511114444");
        // 等号+[
        logger.infoMessage("mobile=[{}]", "13511114444");
        // 英文单引号+等号
        logger.infoMessage("mobile='{}'", "13511114444");
        // 中文冒号
        logger.infoMessage("mobile:{}", "13511114444");
        // 英文冒号
        logger.infoMessage("mobile:{}", "13511114444");
        // 英文双引号+英文冒号
        logger.infoMessage("\"mobile\":\"{}\"", "13511114444");
        // 英文单引号+英文冒号
        logger.infoMessage("'mobile':'{}'", "13511114444");
    }

    /**
     * 基本输出
     */
    @Test
    public void test1() {
        // 11位手机号
        logger.infoMessage("mobile={}", "13511114444");
        logger.infoMessage("mobile={},手机号:{}", "13511112222", "13511113333");
        logger.infoMessage("手机号:{}", "13511115555");
        // 固定电话(带区号-)
        logger.infoMessage("tel:{},座机={}", "0791-83376222", "021-88331234");
        logger.infoMessage("tel:{}", "0791-83376222");
        logger.infoMessage("座机={}", "021-88331234");

        // 地址
        logger.infoMessage("address:{}", "浙江省杭州市滨江区光明大道8888号");
        logger.infoMessage("地址:{}", "上海市浦东区北京东路1-10号");

        // 19位卡号
        logger.infoMessage("cardNo:{}", "6227002020000101222");

        // 姓名
        logger.infoMessage("name={}, 姓名=[{}],name={},姓名:{}", "张三", "上官婉儿", "李云龙", "楚云飞");

        // 密码
        logger.infoMessage("password:{},密码={}", "123456", "456789");

        logger.infoMessage("password:{}", "123456");
        logger.infoMessage("密码={}", "123456");

        // 身份证号码
        logger.infoMessage("idCard:{},身份证号={}", "360123202111111122", "360123202111111122");
        logger.infoMessage("身份证号={}", "360123202111111122");

        // 邮箱
        logger.infoMessage("邮箱:{}", "zhangs12345@google.com");
        logger.infoMessage("email={}", "zhangs12345@google.com");
    }

    /**
     * toString/json输出
     */
    @Test
    public void test2() {
        User user = new User();
        user.setCardNo("6227002020000101222");
        user.setTel("0571-11112222");
        user.setBirth(new Date());

        user.setAddress("浙江省西湖区西湖路288号钱江乐园2-101室");
        user.setEmail("zhangs12345@google.com");
        user.setPassword("123456");
        user.setMobile("15911116789");
        user.setName("张三");
        user.setIdCard("360123202111111122");

        Job job = new Job();
        job.setAddress("浙江省杭州市滨江区某公司");
        job.setTel("0571-12345678");
        job.setJobName("操作员");
        job.setSalary(2000);
        job.setCompany("某某有限公司");
        job.setPosition(Arrays.asList("需求", "开发", "测试", "上线"));

        user.setJob(job);

        logger.infoMessage("用户信息:{}", user);
        logger.infoMessage("用户信息:{}", JSONUtil.toJsonStr(user));
    }
}

test0 输出:

18:19:38.012 INFO  log.test.LogSensitiveTest --- mobile=135****4444
18:19:38.016 INFO  log.test.LogSensitiveTest --- mobile=[135****4444]
18:19:38.017 INFO  log.test.LogSensitiveTest --- mobile='135****4444'
18:19:38.018 INFO  log.test.LogSensitiveTest --- mobile:135****4444
18:19:38.018 INFO  log.test.LogSensitiveTest --- mobile:135****4444
18:19:38.018 INFO  log.test.LogSensitiveTest --- "mobile":"135****4444"
18:19:38.019 INFO  log.test.LogSensitiveTest --- 'mobile':'135****4444'

test1 输出:

18:23:23.115 INFO  log.test.LogSensitiveTest --- mobile=135****4444
18:23:23.115 INFO  log.test.LogSensitiveTest --- mobile=135****2222,手机号:135****3333
18:23:23.115 INFO  log.test.LogSensitiveTest --- 手机号:135****5555
18:23:23.115 INFO  log.test.LogSensitiveTest --- tel:0791-83****22,座机=021-88****34
18:23:23.115 INFO  log.test.LogSensitiveTest --- tel:0791-83****22
18:23:23.115 INFO  log.test.LogSensitiveTest --- 座机=021-88****34
18:23:23.115 INFO  log.test.LogSensitiveTest --- address:浙江省****
18:23:23.115 INFO  log.test.LogSensitiveTest --- 地址:上海市****
18:23:23.115 INFO  log.test.LogSensitiveTest --- cardNo:***************1222
18:23:23.115 INFO  log.test.LogSensitiveTest --- name=张**, 姓名=[上**],name=李**,姓名:楚**
18:23:23.115 INFO  log.test.LogSensitiveTest --- password:******,密码=******
18:23:23.115 INFO  log.test.LogSensitiveTest --- password:******
18:23:23.115 INFO  log.test.LogSensitiveTest --- 密码=******
18:23:23.115 INFO  log.test.LogSensitiveTest --- idCard:3****************2,身份证号=3****************2
18:23:23.115 INFO  log.test.LogSensitiveTest --- 身份证号=3****************2
18:23:23.115 INFO  log.test.LogSensitiveTest --- 邮箱:z****5@google.com
18:23:23.115 INFO  log.test.LogSensitiveTest --- email=z****5@google.com

test2输出:

18:24:55.460 INFO  log.test.LogSensitiveTest --- 用户信息:User{name='张**', idCard='3****************2', cardNo='***************1222', mobile='159****6789', tel='0571-11****22', password='******', email='z****5@google.com', address='浙江省****', birth=Fri May 27 18:24:55 GMT+08:00 2022, job=Job{jobName='操作员', salary=2000, company='某某有限公司', address='浙江省****', tel='0571-12****78', position=[需求, 开发, 测试, 上线]}}
18:24:55.533 INFO  log.test.LogSensitiveTest --- 用户信息:{"address":"浙江省****","idCard":"3****************2","mobile":"159****6789","birth":1653647095451,"cardNo":"***************1222","password":"******","name":"张**","tel":"0571-11****22","job":{"jobName":"操作员","address":"浙江省****","salary":2000,"company":"某某有限公司","tel":"0571-12****78","position":["需求","开发","测试","上线"]},"email":"z****5@google.com"}

参考资料

https://blog.csdn.net/blue_driver/article/details/125007794