切入点

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 {@Testvoid 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 {@Overrideprotected 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接口的实现类,其他的看情况添加可以;

注解方式实现logback日志脱敏相关推荐

  1. Spring Boot基于注解方式处理接口数据脱敏

    1.定义注解 创建Spring Boot项目添加以下依赖 <dependencies><dependency><groupId>org.springframewor ...

  2. logback 日志脱敏 隐藏PII信息

    PII信息全称:Personally identifiable information (PII) PII is any information about an individual maintai ...

  3. logback实现日志脱敏

    许多系统为了安全需要对敏感信息(如手机号.邮箱.姓名.身份证号.密码.卡号.住址等)的日志打印要求脱敏后才能输出,本文将结合个人经历及总结分享一种logback日志脱敏方式,log4j实现日志脱敏请移 ...

  4. SpringBoot整合Logback日志框架+Slf4j注解使用

    文章目录 1.基本介绍 2.使用说明 2.1 引入maven依赖 2.2 创建logback-spring.xml 3.编写一个HTTP接口 3.1 通过创建LoggerFactory实例 3.2 通 ...

  5. logback - 自定义日志脱敏组件,一种不错的脱敏方案

    前言 在我们书写代码的时候,会书写许多日志代码,但是有些敏感数据是需要进行安全脱敏处理的. 对于日志脱敏的方式有很多,常见的有①使用conversionRule标签,继承MessageConverte ...

  6. Spring AOP注解方式实现日志管理

    文章目录 自定义注解 BussLog BussLogAspect 前言:使用注解方式实现日志管理,可以使我们的程序变的清晰.简单,不和很多业务代码混在一起. 实现思路大致分为四点 设计日志表和日志类, ...

  7. spring AOP自定义注解方式实现日志管理

    转:spring AOP自定义注解方式实现日志管理 今天继续实现AOP,到这里我个人认为是最灵活,可扩展的方式了,就拿日志管理来说,用Spring AOP 自定义注解形式实现日志管理.废话不多说,直接 ...

  8. aop注解配置切点 spring_springboot aop 自定义注解方式实现一套完善的日志记录

    一:功能简介 本文主要记录如何使用aop切面的方式来实现日志记录功能. 主要记录的信息有: 操作人,方法名,参数,运行时间,操作类型(增删改查),详细描述,返回值. 二:项目结构图 如果想学习Java ...

  9. java 日志脱敏框架 sensitive,优雅的打印脱敏日志

    问题 为了保证用户的信息安全,敏感信息需要脱敏. 项目开发过程中,每次处理敏感信息的日志问题感觉很麻烦,大部分都是用工具类单独处理,不利于以后统一管理,很不优雅. 于是,就写了一个基于 java 注解 ...

最新文章

  1. Hadoop学习之路(九)HDFS深入理解
  2. 【ArcGIS遇上Python】ArcGIS Python实现Modis NDVI批量求年最大值
  3. linux修改文件没有备份文件,linux文件或目录权限修改后如何恢复(备份了权限就能恢复)...
  4. [密码学基础][每个信息安全博士生应该知道的52件事][Bristol52]46.Sigma协议正确性、公正性和零知识性
  5. 当 IDENTITY_INSERT 设置为 OFF 时,不能向表 中的标识列插入显式值错误的解决方法...
  6. ise verilog多模块编译_如何使用ISE高效开发Verilog项目(新手)
  7. 信息学奥赛一本通(2043:【例5.11】杨辉三角形)
  8. 20150217 IMX257实现GPIO-IRQ中断按键驱动程序
  9. 小鱼易连电脑版_揭秘:为什么win10电脑越用越卡,本质问题是什么?
  10. string类型输入一行字符串,带空格
  11. 小波包分解、重构、去噪与matlab函数使用
  12. win10共享计算机win7,win10与win7局域网共享的方法
  13. AutoCAD三维建模图——汽车车轮
  14. 64位计算机装32位系统,32位装64位系统教程
  15. iphone safaric中将mp4保存到本地相册
  16. 知乎zhihu:Python爬取某个问题下所有含有给定关键词的回答
  17. (FAQ)VM log是做什么的,4 Way VM又是什么
  18. stc15系列c语言pwm编程,STC单片机C语言程序设计 第25章 STC单片机增强型PWM原理及实现.docx...
  19. 辅助知识-第2 章 项目合同管理
  20. app ui设计规范

热门文章

  1. 【BZOJ4861】[Beijing2017]魔法咒语 矩阵乘法+AC自动机+DP
  2. 自然语言处理与应用化学专业的关系
  3. win10安装mysql5.6.35_Win10 64位安装MySQL5.6.35的详细教程
  4. 常见电容器图片_【热门】进口EPCOS电容器原厂品质
  5. 《深入理解java虚拟机》学习笔记--第三章:垃圾收集器与内存分配策略
  6. SGU 231 Prime Sum 求=n内有多少对素数(a,b)使得a+b也为素数 规律题
  7. 基于STM32F407 HAL库的Flash编程操作
  8. 初学构建小项目之仓库管理系统主页面的实现(二)
  9. python转换成exe后一闪而过_解决python xx.py文件点击完之后一闪而过的问题
  10. CentOS8安装最新版本Chromium浏览器