注解方式实现logback日志脱敏

切入点

logback-spring.xml

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>debug</level>
        </filter>
        <encoder>
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            <!-- 设置字符集 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>

思路就是将logback的xml配置文件中的ConsoleAppender、RollingFileAppender替换成我们自己的Appender,通过拦截LoggingEvent,对log方法的入参进行脱敏实现全局控制。看似简单,要做的开箱即用还是需要花点时间。

效果

在需要脱敏的字段上添加自定义注解@Desensitize

@SpringBootTest
@Slf4j
class LogDesensitiveApplicationTests {

    @Test
    void contextLoads() {
        School school = getSchool();
        log.info("{}->msg:{}", System.currentTimeMillis(), school);
    }

    private School getSchool(){
        School school = new School();
        school.setId(1);
        school.setSchoolName("常乐村男子技术学校");
        school.setSchoolAddress("四川省成都市航空港111号");
        Teacher teacher1 = new Teacher();
        teacher1.setTeacherName("lilili");
        teacher1.setTeacherAge(30);
        teacher1.setPhone("18888888888");
        teacher1.setEmail("1334455@qq.com");
        teacher1.setIdCard("33010198704251315");
        teacher1.setBankAccount("6161234589761252");
        Teacher teacher2 = new Teacher();
        teacher2.setTeacherName("zhoushurong");
        teacher2.setTeacherAge(32);
        teacher2.setPhone("16666666666");
        teacher2.setEmail("1334465@qq.com");
        teacher2.setIdCard("33010199904251316");
        teacher2.setBankAccount("6161234589761255");
        Student student = new Student();
        //student.setTeacher(teacher2);
        student.setEmail("1334465@qq.com");
        student.setStuNo("12366666");
        student.setPhone("11122221231");
        student.setStuName("liuchj");
        student.setMoney("2000RMB");
        Map<String,Student> studentMap = new HashMap<>();
        studentMap.put(student.getStuNo(),student);
        teacher2.setStudentMap(studentMap);
        List<Teacher> teacherList = new ArrayList<>();
        teacherList.add(teacher1);
        teacherList.add(teacher2);
        school.setTeacherList(teacherList);
        return school;
    }
}

@Data
class BaseModel {
    int id;
}

@Data
@ToString(callSuper = true)
class School extends BaseModel {
    String schoolName;
    String schoolAddress;
    @Desensitize(type = DesensitizeTypeEnum.COLLECTION)
    List<Teacher> teacherList;
}

@Data
class Teacher {

    String teacherName;

    int teacherAge;

    @Desensitize(type = DesensitizeTypeEnum.PHNOE)
    String phone;

    @Desensitize(type = DesensitizeTypeEnum.EMAIL)
    String email;

    @Desensitize(type = DesensitizeTypeEnum.ACCOUNTNUMBER)
    String bankAccount;

    @Desensitize(type = DesensitizeTypeEnum.ACCOUNTNUMBER)
    String idCard;

    @Desensitize(type = DesensitizeTypeEnum.COLLECTION)
    Map<String,Student> studentMap;
}

@Data
class Student{

    @Desensitize(type = DesensitizeTypeEnum.CUSTOM,length = 1)
    String stuName;

    @Desensitize(type = DesensitizeTypeEnum.CUSTOM,length = 4)
    String stuNo;

    @Desensitize(type = DesensitizeTypeEnum.EMAIL)
    String email;

    @Desensitize(type = DesensitizeTypeEnum.PHNOE)
    String phone;

    @Desensitize(type = DesensitizeTypeEnum.PRICE)
    String money;

}

结果

2022-09-21 22:52:35.357  INFO 13004 --- [           main] c.e.d.LogDesensitiveApplicationTests     : 1663761155357->msg:{teacherList=[{bankAccount=61********761252, teacherAge=30, teacherName=lilili, phone=18****88888, idCard=33********4251315, studentMap=null, email=1****55@qq.com}, {bankAccount=61********761255, teacherAge=32, teacherName=zhoushurong, phone=16****66666, idCard=33********4251316, studentMap={12366666={money=****, phone=11****21231, stuName=liuchj, stuNo=12366666, email=1****65@qq.com}}, email=1****65@qq.com}], schoolAddress=四川省成都市航空港111, schoolName=常乐村男子技术学校}

实现

1、自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface Desensitize {
    /**
     * 字段类型
     */
    DesensitizeTypeEnum type() default DesensitizeTypeEnum.CUSTOM;

    /**
     * 脱敏长度,非custom的取DesensitizeTypeEnum中
     */
    int length() default -1;
}
2、预先定义一些常规的脱敏类型
public enum DesensitizeTypeEnum {

    /**
     * 邮箱
     */
    EMAIL("EMAIL",4),

    /**
     * 电话
     */
    PHNOE("PHNOE",4),

    /**
     * 用户名
     */
    USERNAME("USERNAME",2),

    /**
     * 密码
     */
    PASSWORD("PASSWORD",0),

    /**
     * 账户类(卡号、证件号)
     */
    ACCOUNTNUMBER("ACCOUNTNUMBER",8),

    /**
     * 金额
     */
    PRICE("PRICE",0),

    /**
     * 自定义类型,传入脱敏长度
     */
    CUSTOM("CUSTOM",-1),

    /**
     * 集合字段
     */
    COLLECTION("COLLECTION",-1);

    /**
     * 字段类型,email,username,password,phone等等,也可以是自定义
     */
    private String dataType;

    /**
     * 脱敏长度,0的话全脱敏
     */
    private int length;

    DesensitizeTypeEnum(String dataType,int length ) {
        this.dataType = dataType;
        this.length = length;
    }

    public String getDataType() {
        return dataType;
    }

    public int getLength() {
        return length;
    }
}
3、配置类

脱敏的规则因为是通过注解的方式去配置,常规的配置需要一个项目的包路径,这个后面递归处理日志入参对象时有用,可以通过yml来配置,类似于mybatis配置的mapscan,在这个包下的类会进行脱敏。

public class DesensitizeConfig {

    // 这里可以做成从yml中取,做成start的话必配置,非此包下的类不会被脱敏
    public static final String BASE_PACKAGE = "com.example.desensitive";

}
4、脱敏工具类(核心代码)

入参就是LoggingEvent,这个对象中包含以下几个关键属性:

log.info("hello:{}, I am {}","java","chenxi_lu");
  • argumentArray ----- [“java”,“chenxi_lu”]
  • message ------ “hello:{}, I am {}”
  • formattedMessage ------ “hello:java, I am chenxi_lu”

formattedMessage就是我们最终打出来的日志内容,这个是通过argumentArray 生成的,源码如下:

    public String getFormattedMessage() {
        if (this.formattedMessage != null) {
            return this.formattedMessage;
        } else {
            if (this.argumentArray != null) {
                this.formattedMessage = MessageFormatter.arrayFormat(this.message, this.argumentArray).getMessage();
            } else {
                this.formattedMessage = this.message;
            }

            return this.formattedMessage;
        }
    }

那么方案就有两大种:

  • 直接获取 formattedMessage这个字符串,然后通过正则匹配邮箱数字这些常规的敏感词,然后替换成*;
  • 获取argumentArray中的每个入参,通过反射获类信息,类中的字段信息,把带有自定义注解的字段格式化掉,最后替换掉argumentArray中的元素;这种的话更灵活,不至于一棍子打死全部;
@Slf4j
public class DesensitiveUtil {

    /**
     * 脱敏处理
     */
    public static void operation(LoggingEvent event) throws IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException {
        //  获取log参数,替换占位符{}的入参
        Object[] args =  event.getArgumentArray();
        if(!Objects.isNull(args)){
            for(int i =0; i<args.length; i++){
                Object arg = args[i];
                args[i] = toArgMap(arg);
            }
        }
        event.setArgumentArray(args);
    }

    /**
     * 将参数格式化,非自己工程的包不处理,自己的对象递归处理带注解的属性,进行格式化,替换原来的参数。
     */
    private static Object toArgMap(Object arg) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
        // 通过反射的获取到参数对象
        Class argClass = arg.getClass();
        Package classPath = argClass.getPackage();
        // 不是目标包不处理
        if (!Objects.isNull(classPath) && classPath.getName().startsWith(DesensitizeConfig.BASE_PACKAGE)){
            Map<String,Object> entityMap = new HashMap<>();
            // 获取字段
            entityMap = loop(arg);
            return entityMap;
        }
        return arg;
    }

    /**
     * 递归处理自有类属性,对象相互引用的情况暂时未处理,会抛出堆栈异常。
     */
    private static Map<String,Object> loop(Object arg) throws IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException {
        Class argClass = arg.getClass();
        Map<String,Object> entityMap = new HashMap<>();
        Field[] fields = argClass.getDeclaredFields();
        if(!Objects.isNull(fields)){
            for (int k = 0; k < fields.length; k++) {
                Field field = fields[k];
                field.setAccessible(true);
                //Class fieldClass =  field.getDeclaringClass();
                Class fieldTypeClass =  field.getType();
                Package classPath = fieldTypeClass.getPackage();
                Object fieldValue = field.get(arg);
                if(Objects.isNull(classPath)){
                    // 基本数据类型,int,long这些
                    entityMap.put(field.getName(),fieldValue);
                    continue;
                }
                String fieldClassPath = classPath.getName();
                if(Objects.isNull(fieldValue)){
                    // 空值字段,如果确实有对象相互依赖的,一定要处理(a1==>b==>a1  调整成  a1==>b==>a2),这样a2里的b就是空的。
                    entityMap.put(field.getName(),fieldValue);
                    continue;
                }
                if(fieldClassPath.startsWith(DesensitizeConfig.BASE_PACKAGE)){
                    Map<String,Object> loopEntity = loop(fieldValue);
                    entityMap.put(field.getName(),loopEntity);
                } else {
                   // 判断属性是否带注解
                    Desensitize desensitizeAnnotation =  field.getAnnotation(Desensitize.class);
                    if(Objects.isNull(desensitizeAnnotation)) {
                        // 不脱敏的保存原来值
                        entityMap.put(field.getName(),fieldValue);
                    }else {
                        // 带注解的进行脱敏
                        DesensitizeTypeEnum desensitizeTypeEnum = desensitizeAnnotation.type();
                        int length = desensitizeAnnotation.length() < 0 ? desensitizeTypeEnum.getLength() : desensitizeAnnotation.length();
                        switch (desensitizeTypeEnum){
                            case EMAIL:
                                if(isStr(field)){
                                    String val = desensitizeEmail(fieldValue,length);
                                    entityMap.put(field.getName(),val);
                                }else {
                                    log.error("{} is need String field!",desensitizeTypeEnum.getDataType());
                                    // 不脱敏的保存原来值
                                    entityMap.put(field.getName(),fieldValue);
                                }
                                break;
                            case PHNOE:
                                if(isStr(field)){
                                    String val = desensitizePhone(fieldValue,length);
                                    entityMap.put(field.getName(),val);
                                }else {
                                    log.error("{} is need String field!",desensitizeTypeEnum.getDataType());
                                    // 不脱敏的保存原来值
                                    entityMap.put(field.getName(),fieldValue);
                                }
                                break;
                            case USERNAME:
                                if(isStr(field)){
                                    String val = desensitizeUserName(fieldValue,length);
                                    entityMap.put(field.getName(),val);
                                }else {
                                    log.error("{} is need String field!",desensitizeTypeEnum.getDataType());
                                    // 不脱敏的保存原来值
                                    entityMap.put(field.getName(),fieldValue);
                                }
                                break;
                            case PASSWORD:
                                // 密码全脱敏
                                entityMap.put(field.getName(),"****");
                                break;
                            case ACCOUNTNUMBER:
                                if(isStr(field)){
                                    String val = desensitizeAccountNumber(fieldValue,length);
                                    entityMap.put(field.getName(),val);
                                }else {
                                    log.error("{} is need String field!",desensitizeTypeEnum.getDataType());
                                    // 不脱敏的保存原来值
                                    entityMap.put(field.getName(),fieldValue);
                                }
                                break;
                            case PRICE:
                                // 价格的全脱敏
                                entityMap.put(field.getName(),"****");
                                break;
                            case CUSTOM:
                                entityMap.put(field.getName(),fieldValue);
                                break;
                            case COLLECTION:
                                // 对集合类型的字段,需要判断集合里存的是什么对象
                                entityMap.put(field.getName(),desensitizeCollect(field,fieldValue));
                                break;
                           default:
                               entityMap.put(field.getName(),fieldValue);
                        }
                    }
                }
            }
        }
        return entityMap;
    }

    /**
     * 脱敏嵌套的集合类型
     * 支持:Collection,Map
     */
    private static Object desensitizeCollect(Field field, Object fieldValue) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
        if(fieldValue instanceof Collection){
            Collection coll = (Collection) fieldValue;
            Iterator iterator = coll.iterator();
            Collection desColl = (Collection)fieldValue.getClass().newInstance();
            desColl.clear();
            while(iterator.hasNext()){
                Object item = iterator.next();
                Object desensitizeItem = toArgMap(item);
                desColl.add(desensitizeItem);
            }
            return desColl;
        }else if(fieldValue instanceof Map){
			// Map 字段
            Map<Object,Object> objectMap = (Map)fieldValue;
            Map<Object,Object> desMap = (Map)fieldValue.getClass().newInstance();
            desMap.clear();
            for(Object key : objectMap.keySet()){
                Object deObject = toArgMap(objectMap.get(key));
                desMap.put(key,deObject);
            }
            return desMap;
        }else {
            log.error("{} is not support to desensitize!",field.getType());
        }
        return fieldValue;
    }

    /**
     * 脱敏账号
     */
    private static String desensitizeAccountNumber(Object fieldValue,int length) {
        String account = String.valueOf(fieldValue);
        // 邮箱地址取前半部分
        int accountLength = account.length();
        // 取开始脱敏的位置
        int index = accountLength/length;
        // 如果有邮箱地址特别短的
        int finalLength = accountLength<length?accountLength:length;
        // 替换字符
        char[] chars = account.toCharArray();
        for(int i =index;i<index+finalLength;i++){
            chars[i] = '*';
        }
        return String.valueOf(chars);
    }


    /**
     * 脱敏用户名
     */
    private static String desensitizeUserName(Object fieldValue,int length) {
        String username = String.valueOf(fieldValue);
        // 邮箱地址取前半部分
        int usernameLength = username.length();
        // 取开始脱敏的位置
        int index = usernameLength/length;
        // 如果有邮箱地址特别短的
        int finalLength = usernameLength<length?usernameLength:length;
        // 替换字符
        char[] chars = username.toCharArray();
        for(int i =index;i<index+finalLength;i++){
            chars[i] = '*';
        }
        return String.valueOf(chars);
    }

    /**
     * 脱敏手机号码
     */
    private static String desensitizePhone(Object fieldValue,int length) {
        String phone = String.valueOf(fieldValue);
        // 邮箱地址取前半部分
        int phoneLength = phone.length();
        // 取开始脱敏的位置
        int index = phoneLength/length;
        // 如果有邮箱地址特别短的
        int finalLength = phoneLength<length?phoneLength:length;
        // 替换字符
        char[] chars = phone.toCharArray();
        for(int i =index;i<index+finalLength;i++){
            chars[i] = '*';
        }
        return String.valueOf(chars);
    }

    /**
     * 脱敏邮件
     */
    private static String desensitizeEmail(Object fieldValue,int length) {
        String email = String.valueOf(fieldValue);
        String[] emailName = email.split("@");
        // 邮箱地址取前半部分
        int emailNameLength = emailName[0].length();
        // 取开始脱敏的位置
        int index = emailNameLength/length;
        // 如果有邮箱地址特别短的
        int finalLength = emailNameLength<length?emailNameLength:length;
        // 替换字符
        char[] chars = email.toCharArray();
        for(int i =index;i<index+finalLength;i++){
            chars[i] = '*';
        }
        return String.valueOf(chars);
    }

    /**
     * 判断字段是否是String类型
     */
    private static boolean isStr(Field field){
        return field.getType().equals(String.class);
    }
}
5、自定义Appender
@Slf4j
public class DesensitiveConsoleAppender extends ConsoleAppender {

    @Override
    protected void subAppend(Object event) {
        try {
            DesensitiveUtil.operation((LoggingEvent) event);
        }catch (Exception e){
            log.error("log error!",e);
        }finally {
            super.subAppend(event);
        }
    }

}

其他几个Appender一样。

6、替换logback-spring.xml
    <!--输出到控制台-->
    <appender name="CONSOLE" class="com.example.desensitive.appender.DesensitiveConsoleAppender">
        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>info</level>
        </filter>
        <encoder>
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            <!-- 设置字符集 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>

完成!!

注意点

1、对象间相互引用

暂时处理不了,有大佬知道的请不吝赐教。

	A a = new A();
	B b = new B();
	a.setB(b);
	b.setA(a);
	log.info("a:{}",a);

这种情况下,递归会死掉,堆栈溢出。

2、集合字段

目前只支持Collection与Map接口的实现类,其他的看情况添加可以;

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值