• 注解方式实现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接口的实现类,其他的看情况添加可以;

  • 相关阅读:
    Hazelcast系列(八):数据结构
    员工犯错,就应该受惩罚吗?
    Sql Server查数据库job任务
    2.11 Nginx控制浏览器缓存
    我被 .NET8 JIT 的一个BUG反复折磨了半年之久(JIT tier1 finally optimizations)
    Java 浅析线程池ThreadPoolExecutor
    【数据结构】【程序填空】赫夫曼编码
    R语言caTools包进行数据划分、scale函数进行数据缩放、e1071包的naiveBayes函数构建朴素贝叶斯模型
    音视频从入门到精通——FFmpeg分离出PCM数据实战
    【ELM回归预测】探路者优化极限学习机回归预测【含Matlab源码 2231期】
  • 原文地址:https://blog.csdn.net/fh_luchenxi/article/details/126978207