写点什么

一起进阶一起拿高工资!Java 开发进阶 -log4j2 日志脱敏原理分析

发布于: 2021 年 01 月 24 日
一起进阶一起拿高工资!Java开发进阶-log4j2日志脱敏原理分析

大家好,我是 tin,这是我的第 5 篇原创文章


本文讲述在考虑对业务系统代码入侵最小的情况下实现日志脱敏的方案原理。文章很长,包括了日志脱敏起由、编码实现、log4j2.xml 文件加载原理、log4j2 的插件机制等,最后还抖出注解编译处理器 AbstractProcessor,实现编译期动态生成代码!有点像捡到宝,毕竟以前没关注过注解编译处理器,先上一个目录:


  • 一、为什么做日志脱敏

  • 二、log4j2 日志脱敏编码实现

  • 三、源码探索 log4j2 日志脱敏实现原理

1、什么是 slf4j?

2、log4j2 又是什么?

3、slf4j 和 log4j2 是如何完成绑定的?

4、log4j.xml 配置文件是如何加载的?

5、我们定义 log4j2 的 Plugin 插件又是如何加载注册的?

6、AbstractProcessor 注解处理器

  • 四、朋友请留步

一、为什么做日志脱敏


日志打印非常常见且重要,这毋庸置疑,但有这样一种情况:我们打印的日志包含了用户的隐私信息,比如做登录支付的打印用户账号和密码,做金融的打印用户的卡号等,这些日志先不说放在磁盘上管理不当可能造成用户隐私泄露,再者就算是合规检查,它也是不过关的,必须要做处理整治。


我们打日志是怎么打的?先上一个图(日志中打印我的用户名和账号),看看我们自己就是这么用的:

没做特殊处理,不出意外,日志输出是这样的:

卡号打印出来了,随后这行日志就安详地躺在我们服务器磁盘上。

二、log4j2 日志脱敏编码实现


如何借助日志框架实现对账号打码脱敏,而不入侵业务代码?废话不多说,先看看我已实现的效果:

本文基于 slf4j+log4j2 实现,我们代码日志输出处没有任何改动,打打印出来的日志对卡号做了打码脱敏。


本文实现日志打码脱敏的方案涉及开发的地方有两个:


一是实现 log4j2 的 RewritePolicy 接口,重写 logEvent;


二是修改 log4j2.xml 文件。


看看我写的 RewritePolicy 实现类:

log4j2.xml 修改,下面是 log4j2 配置和 rewrite 配置:

这个文件也非常详细地把 log4j2.xml 配置解释了一遍,不是很清楚 log4j 配置的可留图保存啦。


为了方便复制,把 log4j2.xml 配置的源码粘贴一份出来:

<?xml version="1.0" encoding="UTF-8"?><configuration monitorInterval="5">    <!--变量配置-->    <Properties>        <!-- 格式化输出:%date表示日期,HH:mm:ss.SSS表示日期格式,%thread表示线程名,%-5level表示级别从左显示5个字符宽度,        %C{1.}表示类全限定名输出精度,%-4L输出日志所在行行号,%msg代表日志消息,%n是换行符-->        <property name="LOG_PATTERN" value="%date{HH:mm:ss.SSS} [%thread] %-5level %C{1.} %-4L : %msg%n"/>        <!-- 定义日志存储的路径.${web:rootDir}表示当前工程目录, -->        <property name="FILE_PATH" value="../log/tin-example"/>        <property name="FILE_NAME" value="tin-example"/>    </Properties>
<appenders> <!--控制台输出--> <console name="Console" target="SYSTEM_OUT"> <!--输出日志的格式--> <PatternLayout pattern="${LOG_PATTERN}"/> <!--表示输出level=debug级别及以上日志(onMatch),debug级别以下不输出(onMismatch)--> <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/> </console>
<Rewrite name="rewrite"> <DataMaskingRewritePolicy/> <AppenderRef ref="Console"/> </Rewrite>
<!-- 打印出所有级别的日志信息,并自动滚动存档--> <RollingFile name="AllLevelRollingFile" fileName="${FILE_PATH}/${FILE_NAME}.log" filePattern="${FILE_PATH}/${FILE_NAME}-ALL-%d{yyyy-MM-dd}_%i.log.gz"> <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="ACCEPT"/> <PatternLayout pattern="${LOG_PATTERN}"/> <Policies> <!--interval属性用来指定多久滚动一次,interval=1表示1小时滚动一次--> <TimeBasedTriggeringPolicy interval="1"/> <!--size=20表示文件大于20M滚动一次--> <SizeBasedTriggeringPolicy size="20MB"/> </Policies> <!-- max=15表示同文件夹下最多10个文件,再多则会覆盖,DefaultRolloverStrategy如不设置,则默认为7个--> <DefaultRolloverStrategy max="10"/> </RollingFile>
<!-- 打印出所有error及以上级别的信息,并自动滚动存档--> <RollingFile name="ErrorRollingFile" fileName="${FILE_PATH}/error.log" filePattern="${FILE_PATH}/${FILE_NAME}-ERROR-%d{yyyy-MM-dd}_%i.log.gz"> <!--输出level及以上级别的信息(onMatch),level以下直接拒绝(onMismatch)--> <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="${LOG_PATTERN}"/> <Policies> <!--interval属性用来指定多久滚动一次,interval=1表示1小时滚动一次--> <TimeBasedTriggeringPolicy interval="1"/> <!--size=20表示文件大于20M滚动一次--> <SizeBasedTriggeringPolicy size="20MB"/> </Policies> <!-- max=15表示同文件夹下最多10个文件,再多则会覆盖,DefaultRolloverStrategy如不设置,则默认为7个--> <DefaultRolloverStrategy max="10"/> </RollingFile>
</appenders>
<!--Logger节点用来单独指定日志的形式,可以给不同包配置不同的日志打印策略。--> <loggers> <logger name="com.tin.example.spring.log4j2" level="info" additivity="false"> <AppenderRef ref="rewrite"/> </logger>
<root level="debug"> <appender-ref ref="Console"/> <appender-ref ref="AllLevelRollingFile"/> <appender-ref ref="ErrorRollingFile"/> </root> </loggers></configuration>
复制代码


三、源码探索 log4j2 日志脱敏原理

为何上文这么做就能实现日志打码脱敏?是有什么变法么?实现的原理是什么?背着一大连串疑问,现在我们从 slf4j 和 log4j2 原理说起,来了,搬好凳子了。

1、什么是 slf4j?


slf4j 全称 simple logging facade for Java。是一个日志接口框架,配合日志输出系统实现日志输出。slf4j 并不是真正输出日志的系统,只是定义了一套日志规范。类似这样的日志门面还有 commons-logging。


private static final Logger LOGGER = LoggerFactory.getLogger(AccountTest.class);


以上的 Logger 就是 slf4j 的类。


2、log4j2 又是什么?


log4j2 才是一个真正的日志系统,它才是我们项目中打印日志的代码库实现。除了 log4j2,我们常见的日志库还有 log4j、logback、jdk-logging。


slf4j 作为连接 log 和代码层的中间层,我们只要使用 slf4j 提供的接口,不用关心日志的具体实现(想想这样的好处是我们可以把业务系统内日志库比如 log4j2 换为 logback 也没问题)。说起来有点像 jdbc,我们切换不同的数据库,jdbc 帮我们做好了兼容。


log4j2 的依赖包有 3 个,slf4j 和 log4j2 的几个 jar 包关系作用如下:

3、slf4j 和 log4j2 是如何完成绑定的?


从上面图都看到了,slf4j-api 和 log4j 相关的包根本不在一起,那么它们之间是通过什么关联的?


答案是:


slf4j 指定路径进行类加载,log4j 必然有桥接实现类。 还是从这行定义和初始化 Logger 的代码开始看起:


private static final Logger LOGGER = LoggerFactory.getLogger(AccountTest.class);


从 LoggerFactory.getLogger 一直进入到 LoggerFactory 类的 bind 方法,找到 staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet(),这里即是 slf4j 完成绑定 log4j2 的地方:

findPossibleStaticLoggerBinderPathSet()通过指定路径加载一个 StaticLoggerBinder 类:

指定查找 org/slf4j/impl/StaticLoggerBinder.class 进行加载。


那么 StaticLoggerBinder 应该在哪里?


当然是在 log4j2 包内了!

通过 StaticLoggerBinder 这个类即完成了 slf4j 和 log4j 的绑定,看下图。


绑定完之后即通过 getLoggerFactory 方法获取到 Log4jLoggerFactory:

log4j2 和 slf4j 完成了绑定,那么,和本文所说的脱敏原理有什么关系?


脱敏的实现原理真正在于 log4j2,以上只是展开说明日志系统的基本关联原理,为接下来讲述 log4j 的插件机制打个铺垫。


log4j2 通过使用插件机制加载各种组件,比如 appender, logger 等,我们的脱敏方案编码定义了一个类:

实现了 log4j 的 rewrite 策略类,这其实就是一种插件!


要讲清楚 Plugin 原理得分两部分讲。


一是 log4j.xml 配置文件是如何加载的;


二是我们定义的 Plugin 插件又是如何加载注册的。


4、log4j.xml 配置文件是如何加载的?


我们依然是通过断点看源码,毕竟,源码底下无谎言! 还是从下面这行代码开始看起:

上文已经提到过 Log4jLoggerFactory,它继承了 AbstractLoggerAdapter 这个抽象类,我们直接进入到 getContext 方法获取 Logger 的地方:

anchor 中文译为"锚",这里是通过 Java 反射得到日志类,anchor 不为 null,因此进入到后面的语句。

进入 getContext,我们的 Log4jContextFactory 又出现了,它在 LogManager 中的静态代码块中已初始化好。

我们继续到 Log4jContextFactory 内看 getContext:

已初始化好的 selector,内部具体如何获取 context 有兴趣可自行 debug,我们进入到 ctx.start 方法内:

看到 reconfigure()方法,就知道 log4j 准备开始加载配置了!!!再从 reconfigure 一直往下看:

687 行获取得到一个 XmlConfiguration,这是因为我们使用的是 xml 配置文件!!!正常来说配置文件除了 xml,还有 properties,yaml,json 等。


此处既然已获得配置文件的内容, 那么我们退回去看 ConfigurationFactory.getInstance().getConfiguration(this,contextName,configURI,cl)。

看看 XmlConfigurationFactory 类


指定了 xml 后缀,getConfiguration 实际返回 XmlConfiguration


根据 configSource 的 log4jx.xml 文件,进行配置内容加载。

到这里 xml 配置就算是加载完成啦。xml 里面定义的<DataMaskingRewritePolicy/>标签也会被加载。

接下来,自然而然的我们就会问,这个标签和代码 @Plugin 注解定义的插件是如何关联起来的?或者又说 Plugin 插件是如何加载的?


5、我们定义的 Plugin 插件又是如何加载注册的?


log4j 中的 Plugin 注解提供了一种便捷的方法将一个类声明成 log4j2 的插件,比如我单测用到的案例:

在 log4j2 加载上下文的时候会加载 Plugin,log4j 统一用 PluginRegistry 注册中心加载和注册插件,并由 PluginManager 来管理。


进入到 PluginManager:

注释都写得很清楚,从指定的指定文件 Log4j2Plugin.dat 加载插件,继续进入 loadFromMainClassLoader 方法


不同模块不同 jar 包都有可能存在 Log4j2Plugins.dat 文件,比如 log4j-core 包存在

我们自己编写代码定义的插件则被编译到 target 目录下(因为我的是 mac,在控制台的看得,win 系统也一样找到编译产生的 target 文件夹即可)

我们编译生成的 Log4j2Plugins.dat 里面的内容又是什么呢?

文件记录了插件分类、全限定类名等信息。


说到这里,产生新的一个疑问,我们自己的 Log4j2Plugins.dat 文件究竟是如何被生成到 target 目录下的?


6、AbstractProcessor 注解处理器


这不得不说我们的注解编译处理器咯!注解分为两种类型,一种是运行时注解,另一种是编译时注解。编译时注解的核心要依赖 APT(Annotation Processing Tools)实现,基本原理就是在类、方法、字段等上添加注解,在编译时,编译器通过扫描 AbstractProcessor 的子类,把对应合适的注解传入 process 函数,然后我们开发人员可以在编译期进行相应的逻辑处理了。看看 log4j 实现的注解编译处理器:

我们平常编码很少会用到注解编译处理器,有兴趣可自行写单元测试试一试,这种没玩过的代码写起来还挺有趣的。不过自行写的话需要声明好 javax.annotation.processing.Processor 文件,再补一张 log4j 声明的文件:

四、朋友请留步


我是 tin,一个在努力让自己变得更优秀的普通攻城狮。阅历有限、学识浅薄,如你有发现文章不妥之处,非常欢迎加我提出,我一定细心推敲加以修改。


看到这里请安排个鼓励再走吧,坚持原创不容易,你的正反馈是我坚持输出的最强大动力,谢谢啦。

总结、提升

做一个快乐的攻城狮

构筑属于自己的一方天地


发布于: 2021 年 01 月 24 日阅读数: 38
用户头像

我是tin,公众号:看点代码再上班。 2018.11.13 加入

我是tin,专职后端开发,在这里分享Java相关知识、我的工作经验和工作思考。坚持原创,持续原创,欢迎关注公众号【看点代码再上班】

评论

发布
暂无评论
一起进阶一起拿高工资!Java开发进阶-log4j2日志脱敏原理分析