如何编写Log4j2脱敏插件

1.背景

我所在的公司最近要求须要在全部地方都要脱敏敏感数据,应该是受faceBook数据泄密影响吧。java

说到脱敏通常来讲在数据输出的地方须要脱敏而咱们数据落地输出的地方通常是有三个地方:linux

  • 接口返回值脱敏
  • 日志脱敏
  • 数据库脱敏

这里主要说一下如何进行日志脱敏,对于代码中来讲日志打印敏感数据有两种:面试

  1. 敏感数据在方法参数中
LOGGER.info("person mobile:{}", mobile);复制代码

对于这种建议写个Util直接进行脱敏,由于mobile这个参数名在代码中是没法获取的,当时有想过对传的参数使用正则匹配,这样的话效率过低,会让每一个日志方法都进行正则匹配,效率极低,而且若是恰好有个符合手机号的字符串可是不是敏感信息,这样也被脱敏了。算法

LOGGER.info("person mobile:{}", DesensitizationUtil.mobileDesensitiza(mobile));复制代码

2.敏感数据在参数对象中数据库

Person person = new Person(); 
person.setMobile(mobile); 
LOGGER.info("person :{}", person);复制代码

对于咱们业务中最多的其实就是上面的日志了,为了把整个参数打全,第一种方法须要把参数取出来,第二种只须要传一个参数便可,而后经过toString打印出这个日志,对于这种脱敏有两个方案json

  • 修改toString这个方法,对于修改toString方法又有三个办法:
  1. 直接在toString中修改代码,这种方法很麻烦,效率低,须要修改每个要脱敏的类,或者写个idea插件自动修改toString(),这样很差的地方在于全部编译器都须要开个插件,不够通用。
  2. 在编译时期修改抽象语法树修改toString()方法,就像相似Lombok同样,这个以前调研过,开发难度较大,可能后会更新如何去写。
  3. 在加载的时候经过实现Instrumentation接口 + asm库,修改class文件的字节码,可是有个比较麻烦的地方在于须要给jvm加上启动参数 -javaagent:agent
    jar
    path,这个已经实现了,可是实现后发现的确不够通用。
  • 能够看到上面修改上面toString()方法三个都比较麻烦,咱们能够换个思路,不利用toString()生成日志信息,下面的部分具体解释如何去作。

2.方案

首先咱们要知道当咱们使用LOGGER.info的时候究竟是发生了什么?以下图所示,我这里列举的是异步的状况(咱们项目中都是使用异步,同步效率过低了)。后端


log4j提供给咱们扩展的地方实在是太多了,只要你有需求均可以在里面自定义,好比美团点评本身的xmdt统一日志和线下报警日志都是本身实现的Appender,统一日志也对LogEvent进行了封装。bash

咱们一样也能够利用Log4j2提供给咱们的扩展性,在里面定制化本身的需求。markdown

2.1自定义PatterLayout的Convert

也就是修改上面图中第8步。 经过重写Convert,而且加入过滤逻辑。架构

优势:

这种方法是最理想的,他基本不会影响咱们的日志的性能,由于过滤的逻辑都在PatterLayout里面。

缺点:

可是我在这个地方很尴尬我只能拿到已经生成的String,我只能用笨办法一个词一个词的匹配去搞,而后在修改这个词后面所接的数据进行脱敏,这样太复杂。有想过利用什么算法去优化(好比那些评论系统是若是过滤几万字文章的敏感词的),可是这样成本过高,故而放弃

2.2自定义全局filter

在想到第一个方法的时候,这个时候 实际上是遇到瓶颈了,当时没有彻底分析Log4j2的链路,后面我以为可能从Log4j2全景链路上看,能找到更多的思路,全部便有了上面的图。

上面2.1的方案,为何不大可行呢?主要是我只能拿到已经生成的String了。这个时候我就想我要是能修改String的生成方法就行了,日志其实就是一个字符串而已,具体这个字符串怎么来的不重要。

这个时候我就想到了json,json也是字符串,是咱们数据交换的一种格式。利用生成Json的时候,进行过滤,对咱们须要转换的值进行脱敏从而达到咱们的目的。

固然转换Json和toString()方法,可能两个会有很大效率的差异,这个时候就只能祭出fastjson了,fastjson利用asm字节码技术,摆脱了反射的下降效率,下面的性能基准测试中也已经说明,效率影响基本能够忽略不计。

因此其实咱们就须要两种filter:一个是log4j2的用于脱敏日志的filter,一个是fastjson的filter用于转换Json的时候进行对某些字段作处理。

优势:

改动最小,只须要在Log4j.xml配置文件中添加这个过滤器全局生效,便可使用。

缺点:

1.既然是全局生效,必然会让每一个日志都会从之前的toString转变为json,在追求极端性能的某些服务(好比哪怕多1ms都不可接受)上可能不适用。

2.能够看见咱们这个是在第一步,而第一步的后面是自带的等级过滤器,由于咱们有时候会动态调整日志级别,会致使咱们这个哪怕不是当前可输出等级,他也会进行转换,有点得不偿失。

这个第二点通过优化我把等级过滤器的工做也提早作了,等级不够的直接拒绝。

示例代码以下:

@Plugin(name = "CrmSensitiveFilter", category = Node.CATEGORY, elementType = Filter.ELEMENT_TYPE, printObject = true) 
public class CrmSensitiveFilter extends AbstractFilter { 
    private static final long                       serialVersionUID = 1L; 
 
    private final boolean                           enabled; 
 
 
 
    private CrmSensitiveFilter(final boolean enabled, final Result onMatch, final Result onMismatch) { 
        super(onMatch, onMismatch); 
        //线上线下开关 
        this.enabled = enabled; 
    } 
 
    @Override 
    public Result filter(final Logger logger, final Level level, final Marker marker, final Object msg, 
                         final Throwable t) { 
        return filter(logger, level, marker, null, msg); 
    } 
 
    @Override 
    public Result filter(Logger logger, Level level, Marker marker, String msg, Object... params) { 
        if (this.enabled == false) { 
            return onMatch; 
        } 
        if (level == null || logger.getLevel().intLevel() < level.intLevel()) { 
            return onMismatch; 
        } 
        if (params == null || params.length <= 0) { 
            return super.filter(logger, level, marker, msg, params); 
        } 
        for (int i = 0; i < params.length; i++) { 
            params[i] = deepToString(params[i]); 
        } 
        return onMatch; 
    } 
 
 
 
    @PluginFactory 
    public static CrmSensitiveFilter createFilter(@PluginAttribute("enabled") final Boolean enabled, 
                                                  @PluginAttribute("onMatch") final Result match, 
                                                  @PluginAttribute("onMismatch") final Result mismatch) throws IllegalArgumentException, 
                                                                                                     IllegalAccessException { 
        return new CrmSensitiveFilter(enabled, match, mismatch); 
    } 
}
复制代码


2.3重写MessageFactory

上面全局过滤器的缺点是没法定制化,这个时候我把目光锁定在第三步,生成日志内容输出Message。

经过重写MessageFactory咱们能够生成咱们本身的Message,而且咱们能在代码层面指定咱们的LoggerMannger究竟是使用咱们本身的MesssageFactory,仍是使用默认的,能由咱们本身控制。

固然咱们这里生成的Message基本思路不变依然是fastjson的value过滤器。

优势:

能定制化LOGGER,非全局。

缺点:

局限于Log4j2,其余LogBack等日志框架不适用

下面给出部分代码:

public class DesensitizedMessageFactory extends AbstractMessageFactory { 
    private static final long                      serialVersionUID = 1L; 
 
    /** 
     * Instance of DesensitizedMessageFactory. 
     */ 
    public static final DesensitizedMessageFactory INSTANCE         = new DesensitizedMessageFactory(); 
 
    /** 
     * @param message The message pattern. 
     * @param params The message parameters. 
     * @return The Message. 
     * 
     * @see MessageFactory#newMessage(String, Object...) 
     */ 
    @Override 
    public Message newMessage(String message, Object... params) { 
        return new DesensitizedMessage(message, params); 
    } 
 
    /** 
     * 
     * @param message 
     * @return 
     */ 
    @Override 
    public Message newMessage(Object message) { 
        return new ObjectMessage(DesensitizedMessage.deepToString(message)); 
    } 
}
复制代码

3.使用

咱们团队业务项目以前log4j是使用的2.6版本的,以前是一直是使用的filter,忽然有次升级直接升到2.7,忽然一下脱敏无论用了,当时研究源码发现,filter发生了一些改变当传日志参数小于等于2的时候是有问题的。

须要根据本身业务场景选择一个最适合业务场景的:

log4j版本小于2.6使用filter,大于2.6(固然不大于2.6也能使用)使用MessageFactory

3.1 filter配置(二选一)

找到Log4j.xml(每一个环境都有本身对应的哈)

在最外层节点下面,也就是里面写以下配置,enabled用于线上线下切换,true为生效,false为不生效。

3.2 MessageFactory配置(二选一)

建立文件:log4j2.component.properties

输入:log4j2.messageFactory=log.message.DesensitizedMessageFactory

4.性能基准测试:

基准测试聚焦打印日志效率如何。

硬件:

4核,8G复制代码

操做系统:

linux复制代码

JRE:

v1.8.0_101,初始堆大小4G复制代码

预热策略:

测试开始前,全局预热,执行所有测试若干次,判断运行时间稳定后中止,确保所需class所有加载完成每一个测试开始前,独立预热,重复执行该测试64次,确保JIT编译器充分优化完代码。复制代码

执行策略:

循环执行,初始次数200,以200的步长递增,递增至1000为止。每次执行10次,去掉一个最高,去掉一个最低,取平均值。复制代码

测试结果:

由上面结果可见增加速率基本稳定

上述结果脱敏的时间大概是未脱敏的时间1.5倍,

平均下来未脱敏的是0.1255ms 产生一条,而脱敏的是0.18825ms产生一条日志,二者相差0.06ms左右。

咱们整个请求预估最多有10-20条日志打印,整条请求平均会影响时间0.6ms-1.2ms左右,我以为这个时间能够忽略不计在整个请求当中。

因此这种模式的性能仍是比较好,能够应用于生产环境。 


更多交流请扫个人技术公众号

为了方便你们学习交流,建了个qq java后端交流群:837321192,里面有我收藏的百G学习视频(涵盖面试,架构等等),也有不少面试资料,能够加入进来一块儿交流。   

相关文章
相关标签/搜索