日志脱敏之log4j源码分析(一)
这篇博客提供了一种日志脱敏的实现方式-利用log4j进行脱敏,本文基于log4j 1.2版本。
日志脱敏的几种方式:
- 业务简单,少量日志记录的情况下,可以去手动替换敏感信息
- 大量日志记录的时候,每次调用logger.info都要去考虑脱敏太过麻烦,可以在pojo类里修改toString方法,将敏感数据脱敏
- 大量日志,并且pojo类里有Map类型参数,没办法判断map的value是否是敏感数据。或者日志记录点太多,就需要一种方法,在底层进行脱敏,做到调用方无感知
实际工作中公司可能会使用自己的组件进行日志采集,所以在底层加入脱敏的支持是最好的选择。
这是一个最简单的log4j的demo
import org.apache.log4j.Logger;
public class Demo {
private static Logger logger = Logger.getLogger(Demo.class);
public static void main(String[] args) {
logger.info("start");
// ..do something
logger.info("end");
}
}
首先了解下log4j的结构,log4j的核心类
Logger:提供记录日志的各种api,是最核心的类。
Category:Logger类的爸爸,Logger类继承自父类Category的各种属性,用来做到封装细节,暴露简单api。
Category类中有两个关键类属性:
- 1.LoggerRepository,维护了全部的logger的单例对象(为什么这么做?记录日志的动作是同步得,会影响服务性能,单例对象可以复用,提升服务器性能)他的默认实现是Hierarchy,这个类维护了一个HashTable对象,存放你无数个Controller或者Service里面的静态logger。(同时这个类也实现了log4j的一个关键特性,日志继承,有兴趣的可以看下官方文档,对今天大的分析用处不大,不在赘述)
- 2.AppenderAttachableImpl 好长的名字,不过看意思应该是‘可连接的追加器’,什么意思呢?就是他属于一个管理类,用来管理你的各种Appender,比如ConsoleAppender,FileAppender等等,同时他可以在老的Appender列表后面追加、删除Appender,所以就有了这个名字,个人见解。其实叫AppenderManager,或者AppenderHolder不是更符合常规的命名习惯吗?个性化的命名不太适合开源项目。
OK,关键类了解之后,来看看logger4j的工作流程,下面是从网上找的调用logger.info的时序图,这个是最简单的版本,适合新手学习。先看一眼,不用完全明白,后面再讲。
- 我的工程的依赖
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
- 调用logger.info(“hello”)
- 进入info方法,判断logger级别达到需要记录的级别后,就记录日志,找到forcedLog这行。
- 进入forcedLog方法,他创建了一个新的LoggingEvent对象,调用了callAppenders方法。(这里留心一下,每次都创建新的LoggingEvent对象,是性能不友好的,感兴趣的可以了解下,号称性能优越的Log4j是如何处理LoggingEvent这个类的。)
- 进入callAppenders方法,判断AppenderAttachableImpl 他维护的appenderList不为空后,进入appendLoopOnAppenders这一行
- 进入appendLoopOnAppenders方法。重头戏来了,遍历appenderList的每个appender,进行doAppender操作(这里用到了两种设计模式,有没有人知道?答案在下一篇博客揭晓日志脱敏之Log4j源码分析(二))
- Appender的默认实现AppenderSkeleton(appender骨架。。命名真是特别)doAppender方法实际调用了append方法,在此之前经过了一些Filter的判断,这里的判断就是其中一种设计模式的佐证。
- Appender的实现太多,我们随便选择一个,进入SyslogAppender实现,无视上面的层层判断,看下这行layout.format(event);这里使用Appender对应的layout对event进行了format,后面的操作是这个appender的业务实现,不用太关注。
OK,目前为止,我们已经知道了整个流程,不要忘了我们最初的目的,是要用logger4j的特性进行日志脱敏。
- 首先我们能想到的是自定义Appender,然后在里面自己想怎么脱都行,但是有时候我们使用的是自带的Appender,或者公司自己的Appender组件(比如说flume Appender来做日志收集),重写或者继承这些Appender都会给你的代码带来bad smell。
- 其次,在event处理时,会使用appender对应的layout进行处理。我们可以在layout.format方法里进行脱敏,自定义一个layout太简单不过。
比如对手机号进行脱敏:
public class PhoneMaskLayout extends Layout {
private static Pattern pattern = Pattern.compile("(1[0-9]{2})[0-9]{4}([0-9]{3})");
private static String replace = "$1****$2";
public String format(LoggingEvent event) {
String msg = event.getMessage().toString();
Matcher matcher = pattern.matcher(msg);
if (matcher.find()) {
msg = matcher.replaceAll(replace);
}
return msg;
}
//...
}
至此,看似好像可以了,但是在工作中仅仅实现功能是不可以的,性能,安全都要考虑到,上面说过了,appender的appende方法是同步操作,用正则去匹配手机号会造成严重的性能问题,在下一篇我会写下如何提升性能。
系列博客:
- 日志脱敏之Log4j源码分析(二)
感兴趣的可以看下我的其他博客,包学包会,药到病除…
- Redis排序分页
- Redis实现访问频率限制
- springboot整合spring security前后端分离
- 事务的隔离级别对比学习
- hive udf踩坑