Skip to content

十八、配置文件、接口、日志数据脱敏

核心隐私数据无论对于企业还是用户来说尤其重要,因此要想办法杜绝各种隐私数据的泄漏。

1、配置文件脱敏

经常会遇到这样一种情况:项目的配置文件中总有一些敏感信息,比如数据源的url、用户名、密码....这些信息一旦被暴露那么整个数据库都将会被泄漏,那么如何将这些配置隐藏呢?

以前都是手动将加密之后的配置写入到配置文件中,提取的时候再手动解密,当然这是一种思路,也能解决问题,但是每次都要手动加密、解密不觉得麻烦吗?

今天介绍一种方案,让你在无感知的情况下实现配置文件的加密、解密。利用一款开源插件:jasypt-spring-boot。项目地址如下:

url
https://github.com/ulisesbocchio/jasypt-spring-boot

使用方法很简单,整合Spring Boot 只需要添加一个starter

xml
<dependency>
        <groupId>com.github.ulisesbocchio</groupId>
        <artifactId>jasypt-spring-boot-starter</artifactId>
        <version>3.0.3</version>
</dependency>

配置秘钥

在配置文件中添加一个加密的秘钥(任意),如下:

yaml
jasypt:
  encryptor:
    password: dGNtLW1hbmFnZS1zeXN0ZW

当然将秘钥直接放在配置文件中也是不安全的,我们可以在项目启动的时候配置秘钥,命令如下:

sh
java -jar xxx.jar  -Djasypt.encryptor.password=dGNtLW1hbmFnZS1zeXN0ZW

生成加密后的数据

这一步骤是将配置明文进行加密,代码如下:

java
@SpringBootTest
@RunWith(SpringRunner.class)
public class SpringbootJasyptApplicationTests {

    /**
     * 注入加密方法
     */
    @Autowired
    private StringEncryptor encryptor;

    /**
     * 手动生成密文,此处演示了url,user,password
     */
    @Test
    public void encrypt() {
        String url = encryptor.encrypt("jdbc\\:mysql\\://127.0.0.1\\:3306/test?useUnicode\\=true&characterEncoding\\=UTF-8&zeroDateTimeBehavior\\=convertToNull&useSSL\\=false&allowMultiQueries\\=true&serverTimezone=Asia/Shanghai");
        String name = encryptor.encrypt("root");
        String password = encryptor.encrypt("mac_root");
        System.out.println("database url: " + url);
        System.out.println("database name: " + name);
        System.out.println("database password: " + password);
        Assert.assertTrue(url.length() > 0);
        Assert.assertTrue(name.length() > 0);
        Assert.assertTrue(password.length() > 0);
    }
}

也可以这样

java
public static void main(String[] args) {
    BasicTextEncryptor textEncryptor = new BasicTextEncryptor();
    //加密所需的密钥
    textEncryptor.setPassword("dGNtLW1hbmFnZS1zeXN0ZW0");
    //要加密的数据(数据库的用户名或密码)
    String username = textEncryptor.encrypt("root");
    String password = textEncryptor.encrypt("mac_root");
    String url = textEncryptor.encrypt("jdbc:mysql://10.18.104.78:4000/testdb?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true");
    System.out.println("username:"+username);
    System.out.println("password:"+password);
    System.out.println("url:"+url);
}

将加密后的密文写入配置

jasypt默认使用ENC()包裹,此时的数据源配置如下:

yaml
spring:
  datasource:
    #   数据源基本配置
    username: ENC(ZB2JtabneY/cuH560064Ow==)
    password: ENC(1GRqh9U2S63VDkTUdwA5BQkVrYlAdVES)
    driver-class-name: com.mysql.jdbc.Driver
    url: ENC(d7hUejIq1LhYzbt86uXOw5ZMR9Ko6MUnXxxhFQV2XpSFLPQOSRsu2RAQcdTHagkQnE581NbBL4vZwFl89hn/I8EIiQjISvqceuRnFZ9B7XgJ8OAMqJndWwn+/F3rzF1kmT6OxzeQkd2M+oGjeEeK8EnrE/3TOG5zEvUTuMNc7I9zyWL1v27LtCdRCw0LYwRU2OsfNyem0IonDo/ihXawJcVpRG1RMz0xaCYkE7gVpWN0s8VW9diCxg==)
    type: com.alibaba.druid.pool.DruidDataSource

上述配置是使用默认的prefix=ENC(suffix=),当然我们可以根据自己的要求更改,只需要在配置文件中更改即可,如下:

yaml
jasypt:
  encryptor:
    ## 指定前缀、后缀
    property:
      prefix: 'PASS('
      suffix: ')'

那么此时的配置就必须使用PASS()包裹才会被解密,如下:

yaml
spring:
  datasource:
    #   数据源基本配置
    username: PASS(ZB2JtabneY/cuH560064Ow==)
    password: PASS(1GRqh9U2S63VDkTUdwA5BQkVrYlAdVES)
    driver-class-name: com.mysql.jdbc.Driver
    url: PASS(d7hUejIq1LhYzbt86uXOw5ZMR9Ko6MUnXxxhFQV2XpSFLPQOSRsu2RAQcdTHagkQnE581NbBL4vZwFl89hn/I8EIiQjISvqceuRnFZ9B7XgJ8OAMqJndWwn+/F3rzF1kmT6OxzeQkd2M+oGjeEeK8EnrE/3TOG5zEvUTuMNc7I9zyWL1v27LtCdRCw0LYwRU2OsfNyem0IonDo/ihXawJcVpRG1RMz0xaCYkE7gVpWN0s8VW9diCxg==)
    type: com.alibaba.druid.pool.DruidDataSource

2、接口返回数据脱敏一

1、自定义一个Jackson注解

需要自定义一个脱敏注解,一旦有属性被标注,则进行对应得脱敏,如下:

java
package com.xx.annotation;

import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.xx.enums.PrivacyTypeEnum;
import com.xx.serializer.PrivacySerializer;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Author: xueqimiao
 * @Date: 2023/1/30 13:45
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
// 该注解使用序列化的方式
@JsonSerialize(using = PrivacySerializer.class)
public @interface PrivacyEncrypt {

    /**
     * 脱敏数据类型(没给默认值,所以使用时必须指定type)
     */
    PrivacyTypeEnum type();

    /**
     * 前置不需要打码的长度
     */
    int prefixNoMaskLen() default 1;

    /**
     * 后置不需要打码的长度
     */
    int suffixNoMaskLen() default 1;

    /**
     * 用什么打码
     */
    String symbol() default "*";
}

2、定制脱敏策略

java
package com.xx.enums;

import lombok.Getter;

/**
 * @Author: xueqimiao
 * @Date: 2023/1/30 13:46
 */
@Getter
public enum PrivacyTypeEnum {
    /** 自定义(此项需设置脱敏的范围)*/
    CUSTOMER,

    /** 姓名 */
    NAME,

    /** 身份证号 */
    ID_CARD,

    /** 手机号 */
    PHONE,

    /** 邮箱 */
    EMAIL,
}

3、定制JSON序列化实现

下面将是重要实现,对标注注解@PrivacyEncrypt的字段进行脱敏,实现如下:

java
package com.xx.serializer;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.xx.annotation.PrivacyEncrypt;
import com.xx.enums.PrivacyTypeEnum;
import com.xx.utils.PrivacyUtil;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;

import java.io.IOException;
import java.util.Objects;

/**
 * @Author: xueqimiao
 * @Date: 2023/1/30 13:45
 */
@NoArgsConstructor
@AllArgsConstructor
public class PrivacySerializer extends JsonSerializer<String> implements ContextualSerializer {

    /**
     * 脱敏类型
     */
    private PrivacyTypeEnum privacyTypeEnum;

    /**
     * 前几位不脱敏
     */
    private Integer prefixNoMaskLen;

    /**
     * 最后几位不脱敏
     */
    private Integer suffixNoMaskLen;

    /**
     * 用什么打码
     */
    private String symbol;

    @Override
    public void serialize(String origin, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        if (StringUtils.isNotBlank(origin) && null != privacyTypeEnum) {
            switch (privacyTypeEnum) {
                case CUSTOMER:
                    jsonGenerator.writeString(PrivacyUtil.desValue(origin, prefixNoMaskLen, suffixNoMaskLen, symbol));
                    break;
                case NAME:
                    jsonGenerator.writeString(PrivacyUtil.hideChineseName(origin));
                    break;
                case ID_CARD:
                    jsonGenerator.writeString(PrivacyUtil.hideIDCard(origin));
                    break;
                case PHONE:
                    jsonGenerator.writeString(PrivacyUtil.hidePhone(origin));
                    break;
                case EMAIL:
                    jsonGenerator.writeString(PrivacyUtil.hideEmail(origin));
                    break;
                default:
                    throw new IllegalArgumentException("unknown privacy type enum " + privacyTypeEnum);
            }
        }
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
        if (beanProperty != null) {
            if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
                PrivacyEncrypt privacyEncrypt = beanProperty.getAnnotation(PrivacyEncrypt.class);
                if (privacyEncrypt == null) {
                    privacyEncrypt = beanProperty.getContextAnnotation(PrivacyEncrypt.class);
                }
                if (privacyEncrypt != null) {
                    return new PrivacySerializer(privacyEncrypt.type(), privacyEncrypt.prefixNoMaskLen(),
                            privacyEncrypt.suffixNoMaskLen(), privacyEncrypt.symbol());
                }
            }
            return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
        }
        return serializerProvider.findNullValueSerializer(null);

    }
}

4、定义Person类,对其数据脱敏

使用注解@Sensitive注解进行数据脱敏,代码如下:

java
package com.xx.model;

import com.xx.annotation.PrivacyEncrypt;
import com.xx.annotation.Sensitive;
import com.xx.enums.PrivacyTypeEnum;
import com.xx.enums.SensitiveStrategy;
import lombok.Data;

/**
 * @Author: xueqimiao
 * @Date: 2023/1/30 13:36
 */
@Data
public class Person {

    /**
     * 真实姓名
     */
    private String realName;
    /**
     * 地址
     */
    private String address;
    /**
     * 电话号码
     */
    @PrivacyEncrypt(type = PrivacyTypeEnum.PHONE)
    private String phoneNumber;
    /**
     * 身份证号码
     */
    @PrivacyEncrypt(type = PrivacyTypeEnum.ID_CARD)
    private String idCard;
}

5、模拟接口测试

java
@RestController
public class TestController {

    @GetMapping("/test")
    public Person test() {
        Person user = new Person();
        user.setRealName("小薛博客");
        user.setPhoneNumber("17683456578");
        user.setAddress("广东省深圳市南山区....");
        user.setIdCard("4333333333334334333");
        return user;
    }

}
json
{
    "realName": "小薛博客",
    "address": "广东省深圳市南山区....",
    "phoneNumber": "176****6578",
    "idCard": "4333****34333"
}

3、接口返回数据脱敏二

1、自定义一个Jackson注解

需要自定义一个脱敏注解,一旦有属性被标注,则进行对应得脱敏,如下:

java
package com.xx.annotation;

import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.xx.enums.SensitiveStrategy;
import com.xx.serializer.SensitiveJsonSerializer;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Author: xueqimiao
 * @Date: 2023/1/30 13:34
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveJsonSerializer.class)
public @interface Sensitive {
    //脱敏策略
    SensitiveStrategy strategy();
}

2、定制脱敏策略

针对项目需求,定制不同字段的脱敏规则,比如手机号中间几位用*替代,如下:

java
package com.xx.enums;

import java.util.function.Function;

/**
 * @Author: xueqimiao
 * @Date: 2023/1/30 13:35
 */
public enum SensitiveStrategy {

    /**
     * 用户名
     */
    USERNAME(s -> s.replaceAll("(\\S)\\S(\\S*)", "$1*$2")),
    /**
     * 身份证
     */
    ID_CARD(s -> s.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1****$2")),
    /**
     * 手机号
     */
    PHONE(s -> s.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")),
    /**
     * 地址
     */
    ADDRESS(s -> s.replaceAll("(\\S{3})\\S{2}(\\S*)\\S{2}", "$1****$2****"));


    private final Function<String, String> desensitizer;

    SensitiveStrategy(Function<String, String> desensitizer) {
        this.desensitizer = desensitizer;
    }

    public Function<String, String> desensitizer() {
        return desensitizer;
    }
}

3、定制JSON序列化实现

下面将是重要实现,对标注注解@Sensitive的字段进行脱敏,实现如下:

java
package com.xx.serializer;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.xx.annotation.Sensitive;
import com.xx.enums.SensitiveStrategy;

import java.io.IOException;
import java.util.Objects;

/**
 * @Author: xueqimiao
 * @Date: 2023/1/30 13:35
 */
public class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {
    private SensitiveStrategy strategy;

    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeString(strategy.desensitizer().apply(value));
    }

    /**
     * 获取属性上的注解属性
     */
    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {

        Sensitive annotation = property.getAnnotation(Sensitive.class);
        if (Objects.nonNull(annotation)&&Objects.equals(String.class, property.getType().getRawClass())) {
            this.strategy = annotation.strategy();
            return this;
        }
        return prov.findValueSerializer(property.getType(), property);

    }
}

4、定义Person类,对其数据脱敏

使用注解@Sensitive注解进行数据脱敏,代码如下:

java
package com.xx.model;

import com.xx.annotation.Sensitive;
import com.xx.enums.SensitiveStrategy;
import lombok.Data;

/**
 * @Author: xueqimiao
 * @Date: 2023/1/30 13:36
 */
@Data
public class Person {

    /**
     * 真实姓名
     */
    @Sensitive(strategy = SensitiveStrategy.USERNAME)
    private String realName;
    /**
     * 地址
     */
    @Sensitive(strategy = SensitiveStrategy.ADDRESS)
    private String address;
    /**
     * 电话号码
     */
    @Sensitive(strategy = SensitiveStrategy.PHONE)
    private String phoneNumber;
    /**
     * 身份证号码
     */
    @Sensitive(strategy = SensitiveStrategy.ID_CARD)
    private String idCard;
}

5、模拟接口测试

java
@RestController
public class TestController {

    @GetMapping("/test")
    public Person test() {
        Person user = new Person();
        user.setRealName("小薛博客");
        user.setPhoneNumber("17683456578");
        user.setAddress("广东省深圳市南山区....");
        user.setIdCard("4333333333334334333");
        return user;
    }

}
json
{
    "realName": "小*博客",
    "address": "广东省****市南山区..****",
    "phoneNumber": "176****6578",
    "idCard": "4333****34333"
}

4、日志脱敏

1、引入log4j

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions><!-- 去掉springboot默认配置 -->
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<!--使用log4j2替换 LogBack-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

2、新建log4j2.xml

xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="DEBUG">
    <!-- 定义日志存放目录 -->
    <properties>
        <property name="logPath">logs</property>
        <property name="PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%t-%L] %-5level %logger{36} %L %M - %msg%xEx%n</property>
        <!--"%-d{yyyy-MM-dd HH:mm:ss} [%thread] %m%n"-->
    </properties>
    <!--先定义所有的appender(输出器) -->
    <Appenders>
        <!--输出到控制台 -->
        <Console name="ConsoleLog" target="SYSTEM_OUT">
            <!--只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch) -->
            <ThresholdFilter level="TRACE" onMatch="ACCEPT" onMismatch="DENY" />
            <!--输出日志的格式,引用自定义模板 PATTERN -->
            <CustomPatternLayout pattern="${PATTERN}" />
        </Console>
        <RollingFile name="APPLog" fileName="${logPath}/log_app.log" filePattern="${logPath}/log_%d{yyyy-MM-dd}.log">
            <ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY" />
            <!--<CustomPatternLayout pattern="%-d{yyyy-MM-dd HH:mm:ss} [%thread] %m%n"/>-->
            <CustomPatternLayout pattern="${PATTERN}" />
            <Policies>
                <TimeBasedTriggeringPolicy modulate="true" interval="1"/>
            </Policies>

            <CronTriggeringPolicy schedule="0 0 5 * * ? "/>
            <DefaultRolloverStrategy>
                <Delete basePath="${logPath}" maxDepth="1">
                    <IfFileName glob="log_app_*.log" />
                    <!--删除15天前的文件-->
                    <IfLastModified age="15d" />
                </Delete>

            </DefaultRolloverStrategy>

        </RollingFile>
        <!-- 把error等级记录到文件 一般不用 -->
        <File name="ERRORLog" fileName="${logPath}/error.log">
            <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY" />
            <CustomPatternLayout pattern="${PATTERN}" />
        </File>
    </Appenders>
    <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效 -->
    <Loggers>
        <!--建立一个默认的Root的logger,记录大于level高于warn的信息,如果这里的level高于Appenders中的,则Appenders中也是以此等级为起点,比如,这里level="fatal",则Appenders中只出现fatal信息 -->
        <!-- 生产环境level>=warn -->
        <Root level="debug">
            <!-- 输出器,可选上面定义的任何项组合,或全选,做到可随意定制 -->
            <appender-ref ref="ConsoleLog" />
            <appender-ref ref="ERRORLog" />
            <appender-ref ref="APPLog" />
        </Root>
        <!-- 第三方日志系统 -->
        <!--过滤掉spring和mybatis的一些无用的DEBUG信息,也可以在spring boot 的logging.level.org.springframework=FATAL设置-->
        <logger name="org.springframework" level="DEBUG"></logger>
         <logger name="java.sql.Connection" level="DEBUG"></logger>
         <logger name="java.sql.Statement" level="DEBUG"></logger>
         <logger name="java.sql.PreparedStatement" level="DEBUG"></logger>
    </Loggers>
</Configuration>

3、新建日志规则

java
package com.xx.log4j;

import java.util.HashMap;
import java.util.Map;

/**
 * @Author: xueqimiao
 * @Date: 2023/1/30 13:56、
 * 现在拦截加密的日志有三类:
 * 1,身份证
 * 2,姓名
 * 3,身份证号
 * 加密的规则后续可以优化在配置文件中
 */
public class Log4j2Rule {

    /**
     * 正则匹配 关键词 类别
     */
    public static Map<String, String> regularMap = new HashMap<>();
    /**
     * TODO  可配置
     * 此项可以后期放在配置项中
     */
    public static final String USER_NAME_STR = "Name,name,联系人,姓名";
    public static final String USER_IDCARD_STR = "empCard,idCard,身份证,证件号";
    public static final String USER_PHONE_STR = "mobile,Phone,phone,电话,手机";

    /**
     * 正则匹配,自己根据业务要求自定义
     */
    private static String IDCARD_REGEXP = "(\\d{17}[0-9Xx]|\\d{14}[0-9Xx])";
    private static String USERNAME_REGEXP = "[\\u4e00-\\u9fa5]{2,4}";
    private static String PHONE_REGEXP = "(?<!\\d)(?:(?:1[3456789]\\d{9})|(?:861[356789]\\d{9}))(?!\\d)";

    static {
        regularMap.put(USER_NAME_STR, USERNAME_REGEXP);
        regularMap.put(USER_IDCARD_STR, IDCARD_REGEXP);
        regularMap.put(USER_PHONE_STR, PHONE_REGEXP);
    }
}

4、新建日志插件

java
package com.xx.log4j;

import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Node;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.layout.AbstractStringLayout;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @Author: xueqimiao
 * @Date: 2023/1/30 13:55
 */
@Plugin(name = "CustomPatternLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
public class CustomPatternLayout extends AbstractStringLayout {
    public final static Logger logger = LoggerFactory.getLogger(CustomPatternLayout.class);
    private PatternLayout patternLayout;


    protected CustomPatternLayout(Charset charset, String pattern) {
        super(charset);
        patternLayout = PatternLayout.newBuilder().withPattern(pattern).build();
        initRule();
    }

    /**
     * 要匹配的正则表达式map
     */
    private static Map<String, Pattern> REG_PATTERN_MAP = new HashMap<>();
    private static Map<String, String> KEY_REG_MAP = new HashMap<>();


    private void initRule() {
        try {
            if (MapUtils.isEmpty(Log4j2Rule.regularMap)) {
                return;
            }
            Log4j2Rule.regularMap.forEach((a, b) -> {
                if (StringUtils.isNotBlank(a)) {
                    Map<String, String> collect = Arrays.stream(a.split(",")).collect(Collectors.toMap(c -> c, w -> b, (key1, key2) -> key1));
                    KEY_REG_MAP.putAll(collect);
                }
                Pattern compile = Pattern.compile(b);
                REG_PATTERN_MAP.put(b, compile);
            });

        } catch (Exception e) {
            logger.info(">>>>>> 初始化日志脱敏规则失败 ERROR:{}", e);
        }

    }

    /**
     * 处理日志信息,进行脱敏
     * 1.判断配置文件中是否已经配置需要脱敏字段
     * 2.判断内容是否有需要脱敏的敏感信息
     * 2.1 没有需要脱敏信息直接返回
     * 2.2 处理: 身份证 ,姓名,手机号敏感信息
     */
    public String hideMarkLog(String logStr) {
        try {
            //1.判断配置文件中是否已经配置需要脱敏字段
            if (StringUtils.isBlank(logStr) || MapUtils.isEmpty(KEY_REG_MAP) || MapUtils.isEmpty(REG_PATTERN_MAP)) {
                return logStr;
            }
            //2.判断内容是否有需要脱敏的敏感信息
            Set<String> charKeys = KEY_REG_MAP.keySet();
            for (String key : charKeys) {
                if (logStr.contains(key)) {
                    String regExp = KEY_REG_MAP.get(key);
                    logStr = matchingAndEncrypt(logStr, regExp, key);
                }
            }
            return logStr;
        } catch (Exception e) {
            logger.info(">>>>>>>>> 脱敏处理异常 ERROR:{}", e);
            //如果抛出异常为了不影响流程,直接返回原信息
            return logStr;
        }
    }

    /**
     * 正则匹配对应的对象。
     *
     * @param msg
     * @param regExp
     * @return
     */
    private static String matchingAndEncrypt(String msg, String regExp, String key) {
        Pattern pattern = REG_PATTERN_MAP.get(regExp);
        if (pattern == null) {
            logger.info(">>> logger 没有匹配到对应的正则表达式 ");
            return msg;
        }
        Matcher matcher = pattern.matcher(msg);
        int length = key.length() + 5;
        boolean contains = Log4j2Rule.USER_NAME_STR.contains(key);
        String hiddenStr = "";
        while (matcher.find()) {
            String originStr = matcher.group();
            if (contains) {
                // 计算关键词和需要脱敏词的距离小于5。
                int i = msg.indexOf(originStr);
                if (i < 0) {
                    continue;
                }
                int span = i - length;
                int startIndex = span >= 0 ? span : 0;
                String substring = msg.substring(startIndex, i);
                if (StringUtils.isBlank(substring) || !substring.contains(key)) {
                    continue;
                }
                hiddenStr = hideMarkStr(originStr);
                msg = msg.replace(originStr, hiddenStr);
            } else {
                hiddenStr = hideMarkStr(originStr);
                msg = msg.replace(originStr, hiddenStr);
            }

        }
        return msg;
    }

    /**
     * 标记敏感文字规则
     *
     * @param needHideMark
     * @return
     */
    private static String hideMarkStr(String needHideMark) {
        if (StringUtils.isBlank(needHideMark)) {
            return "";
        }
        int startSize = 0, endSize = 0, mark = 0, length = needHideMark.length();

        StringBuffer hideRegBuffer = new StringBuffer("(\\S{");
        StringBuffer replaceSb = new StringBuffer("$1");

        if (length > 4) {
            int i = length / 3;
            startSize = i;
            endSize = i;
        } else {
            startSize = 1;
            endSize = 0;
        }

        mark = length - startSize - endSize;
        for (int i = 0; i < mark; i++) {
            replaceSb.append("*");
        }
        hideRegBuffer.append(startSize).append("})\\S*(\\S{").append(endSize).append("})");
        replaceSb.append("$2");
        needHideMark = needHideMark.replaceAll(hideRegBuffer.toString(), replaceSb.toString());
        return needHideMark;
    }


    /**
     * 创建插件
     */
    @PluginFactory
    public static Layout createLayout(@PluginAttribute(value = "pattern") final String pattern,
                                      @PluginAttribute(value = "charset") final Charset charset) {
        return new CustomPatternLayout(charset, pattern);
    }


    @Override
    public String toSerializable(LogEvent event) {
        return hideMarkLog(patternLayout.toSerializable(event));
    }
}

5、新建接口测试

java
package com.xx.controller;

import com.xx.model.Person;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author: xueqimiao
 * @Date: 2023/1/30 13:37
 */
@RestController
@Slf4j
public class TestController {

    @GetMapping("/test")
    public Person test() {
        Person user = new Person();
        user.setRealName("小薛博客");
        user.setPhoneNumber("17683456578");
        user.setAddress("广东省深圳市南山区....");
        user.setIdCard("4333333333334334333");
        log.info("----------->"+user.toString());
        return user;
    }
}
java
2023-01-30 13:57:13.978 [] [http-nio-8080-exec-1-23] INFO  com.xx.controller.TestController 23 test - ----------->Person(realName=***, address=广东省深圳市南山区...., phoneNumber=176*****578, idCard=433333******4334333)