SpringBoot+logback 日志打印脱敏,正常获取对象不受影响

添加依赖

注意:springboot版本2.7.0

复制代码
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.36</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.11</version>
        </dependency>

自定义枚举值

复制代码
/**
 * 脱敏枚举值
 */
public enum SensitiveType {
    DEFAULT,    // 默认规则(部分隐藏)
    PHONE,      // 手机号
    ID_CARD,    // 身份证号
    EMAIL,       // 邮箱
    ADDRESS,     //地址
    USER_NAME,  //用户名
    NUMBER;     //数字,直接全脱敏
}

自定义注解

复制代码
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Sensitive {
    SensitiveType type() default SensitiveType.DEFAULT;
}

重写ClassicConverter

复制代码
import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import com.ybchen.log.Sensitive;
import com.ybchen.log.SensitiveType;
import org.slf4j.helpers.MessageFormatter;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

/**
 * @description: 敏感日志转换器
 * @author: 陈彦斌
 * @create: 2024-12-09 18:55
 */
public class SensitiveLogConverter extends ClassicConverter {

    @Override
    public String convert(ILoggingEvent event) {
        Object[] args = event.getArgumentArray();
        if (args == null || args.length == 0) {
            return event.getFormattedMessage();
        }
        for (int i = 0; i < args.length; i++) {
            Object arg = args[i];
            if (arg != null) {
                args[i] = desensitize(arg);
            }
        }
        return MessageFormatter.arrayFormat(event.getMessage(), args).getMessage();
    }

    private Object desensitize(Object obj) {
        if (obj == null) {
            return null;
        }
        try {
            Class<?> clazz = obj.getClass();
            // 如果对象是集合类型
            if (obj instanceof List) {
                List<?> list = (List<?>) obj;
                List<Object> newList = new ArrayList<>();
                for (Object item : list) {
                    newList.add(desensitize(item)); // 递归处理列表中的每个对象
                }
                return newList;
            }
            // 如果对象是普通类型,直接返回
            if (isPrimitiveOrWrapper(clazz) || clazz == String.class) {
                return obj;
            }
            // 创建新实例
            Object newObj = clazz.getDeclaredConstructor().newInstance();
            // 遍历字段
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
                field.setAccessible(true);
                Object fieldValue = field.get(obj); // 获取原字段值
                if (fieldValue == null){
                    continue;
                }
                if (field.isAnnotationPresent(Sensitive.class)) {
                    // 处理标注为 @Sensitive 的字段
                    if (fieldValue instanceof String) {
                        Sensitive sensitive = field.getAnnotation(Sensitive.class);
                        SensitiveType type = sensitive.type();
                        field.set(newObj, applyDesensitization((String) fieldValue, type));
                    }else if (
                            fieldValue instanceof Integer
                                    || fieldValue instanceof Long
                                    || fieldValue instanceof BigDecimal
                                    || fieldValue instanceof Byte
                                    || fieldValue instanceof Short
                                    || fieldValue instanceof Float
                                    || fieldValue instanceof Double
                    ){
                        //数字直接脱敏
                        field.set(newObj, null);
                    }else if (fieldValue instanceof List) {
                        // 如果字段是 List 类型,递归处理其内部元素
                        List<?> list = (List<?>) fieldValue;
                        List<Object> newList = new ArrayList<>();
                        for (Object item : list) {
                            newList.add(desensitize(item));
                        }
                        field.set(newObj, newList);
                    } else {
                        // 对其他非字符串类型的字段,递归脱敏
                        field.set(newObj, desensitize(fieldValue));
                    }
                } else {
                    // 未标注 @Sensitive 的字段直接复制
                    field.set(newObj, fieldValue);
                }
            }
            return newObj; // 返回脱敏后的新对象
        } catch (Exception e) {
            e.printStackTrace();
            return obj; // 异常情况下返回原对象
        }
    }


    private String applyDesensitization(String value, SensitiveType type) {
        if (value==null || "".equals(value)){
            return "";
        }
        switch (type) {
            case DEFAULT:
                return value.replaceAll("^(.{3}).*$", "$1*****");
            case PHONE:
                if (value.length()<11||value.length()>11){
                    return value.replaceAll("^(.{5}).*$", "$1*****");
                }
                return value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
            case ID_CARD:
                if (value.length()<18){
                    return value.replaceAll("^(.{5}).*$", "$1*****");
                }
                return value.replaceAll("(\\d{4})\\d{10}(\\d{4})", "$1**********$2");
            case EMAIL:
                if (!value.contains("@")){
                    return value.replaceAll("^(.{5}).*$", "$1*****");
                }
                return value.replaceAll("(?<=^.{3}).*(?=@)", "*****");
            case ADDRESS:
                if (value.length() <= 9) {
                    return value.replaceAll("(.)(.*)(..)", "$1*$2$3");
                }
                return value.replaceAll("(.{3}).*(.{3})", "$1*****$2");
            case USER_NAME:
                return value.replaceAll("(\\S)\\S(\\S*)", "$1*$2");
            default:
                return value;
        }
    }

    private boolean isPrimitiveOrWrapper(Class<?> clazz) {
        return clazz.isPrimitive() || clazz == Boolean.class || clazz == Integer.class ||
                clazz == Long.class || clazz == Double.class || clazz == Float.class ||
                clazz == Byte.class || clazz == Short.class || clazz == BigDecimal.class || clazz == Character.class;
    }
}

logback-spring.xml配置

复制代码
<configuration>
    <conversionRule conversionWord="sensitive" converterClass="com.ybchen.config.SensitiveLogConverter" />

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %sensitive{%msg}%n</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

VO类

复制代码
@Data
public class UserVO {
    @Sensitive(type = SensitiveType.USER_NAME)
    private String userName;
    @Sensitive(type = SensitiveType.EMAIL)
    private String email;
    @Sensitive(type = SensitiveType.PHONE)
    private String phone;
    @Sensitive(type = SensitiveType.ID_CARD)
    private String idCard;
    @Sensitive(type = SensitiveType.ADDRESS)
    private String address;
    @Sensitive
    private Integer age;
    @Sensitive
    private BigDecimal money;
    @Sensitive
    private String content;
}

打印日志

复制代码
    @PostMapping("test")
    public Object test(@RequestBody List<UserVO> userList){
        UserVO vo=new UserVO();
        vo.setUserName("张三");
        vo.setAddress("广东省广州市天河区xxxxxx号");
        vo.setAge(18);
        log.info("日志脱敏----info:\r\n{}\r\n{}", userList,vo);
        log.info("\r\n");
        log.error("日志脱敏----error:\r\n{}\r\n{}", userList,vo);
        System.out.println("正常获取脱敏数据 userName:"+vo.getUserName());
        System.out.println("正常获取脱敏数据 address:"+vo.getAddress());
        return userList;
    }

效果