如何編寫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學習視頻(涵蓋面試,架構等等),也有不少面試資料,能夠加入進來一塊兒交流。