logback日志脫敏


基於logback實現日志格式規范和脫敏

博客分類:
 

    我們在日常開發中,經常會使用logback打印日志,經常會在日志中打印比如手機號、卡號、郵箱等敏感信息,對數據安全而言是有風險的;但是如果業務程序如果處理這些問題,則需要在每個打印日志的地方都需要進行重復的脫敏操作,繁瑣而且影響代碼風格,還會有遺漏情況;此時我們可能需要考慮一個相對統一的解決方案,那就是增強logback底層的特性,在日志message落盤之前統一進行檢測、脫敏。

 

    我們通常的日志處理面臨的通用訴求:

    1)超長日志message截取:程序打印的日志message可能非常大,比如超過1M,這種message極大的影響系統的性能,而且通常數據價值比較低。我們應該對這種message進行截取或者直接拋棄。

    2)日志格式:通常情況下,我們的production環境的業務日志通過會按需采集、分析、存儲,那么日志格式的統一對下游數據處理是非常必要的;為了避免各種原因錯誤配置了日志格式,我們應該將日志格式規范進行默認集成且限制修改。(我司目前支持兩種格式:普通業務日志,通用數據集(監控指標)類日志)

    日志格式中,通常包含一些用於數據分揀的系統信息(項目名、部署集群名、IP、雲平台、rack等),也包含一些運行時的MDC動態參數值,最終格式是一致的。

    3)脫敏:日志中存在特定規則的字符串時,比如手機號,需要對其進行脫敏處理。

 

    設計核心思想:

    1)基於PatternLayoutEncoder來實現日志格式的限定,不再使用默認的pattern參數指定格式,而是固定字段格式 + 自定義字段,最終拼接成格式規范。

    其中局部可控字段,可以是系統變量、也可以MDC字段列表;固定格式部分,通常是message的頭部,統一包含時間、IP、項目名等等。

    2)基於logback提供的MessageConverter特性,在message打印之前允許對“參數格式化之后的message”(formattedMessage)進行轉換,最終logger打印的實際內容是converter返回的整形后的結果。

    那么,我們就可以基於此特性,在convert方法中執行“超長message截取”、“內容脫敏”兩個主要操作。

 

    主要類列表(新增類):

    1)CommonPatternLayoutEncoder:父類為PatternLayoutEncoder,用於定義日志格式,包括固定字段部分、自定義字段部分,將系統屬性、MDC屬性等,進行拼接,同時基於logback的option特性,將動態參數傳遞給MessageConverter;最終拼接成一個字符串,作為pattern屬性。同時converter所需要的配置參數,比如消息最大長度、正則表達式、替換策略,都需要通過Encoder聲明。

    2)ComplexMessageConverter:message轉換,只會操作logger.info(String message,Throwable ex)傳遞的message部分,其中throwable棧信息不會被操作(其實也無法修改)。

    Converter可以獲取Encoder傳遞的option參數列表,並初始化相關的處理類;內部實現基於正則表達式來匹配敏感信息。

    3)DataSetPatternLayoutEncoder(可選):主要用於限定數據集類的日志格式,它本身不能對敏感信息進行過濾;數據格式主要為了便於數據分析。



  

    1、CommonPatternLayoutEncoder.java

Java代碼   收藏代碼
  1. package ch.qos.logback.classic.encoder;  
  2.   
  3. import ch.qos.logback.classic.PolicyEnum;  
  4. import ch.qos.logback.classic.Utils;  
  5.   
  6. import java.text.MessageFormat;  
  7.   
  8. import static ch.qos.logback.classic.Utils.DOMAIN_DELIMITER;  
  9. import static ch.qos.logback.classic.Utils.FIELD_DELIMITER;  
  10.   
  11. /** 
  12.  * @author liuguanqing 
  13.  * created 2018/6/22 下午8:01 
  14.  * 適用於基於File的Appender 
  15.  * <p> 
  16.  * 限定我司日志規范,增加有關敏感信息的過濾。 
  17.  * 可以通過regex指定需要匹配和過濾的表達式,對於符合表達式的字符串,則采用policy進行處理。 
  18.  * 1)replace:替換,將字符串替換為facade,比如:18611001100 > 186****1100 
  19.  * 2) drop:拋棄整條日志 
  20.  * 3)erase:擦除字符串,全部替換成等長度的"****",18611001100 > *********** 
  21.  * <p> 
  22.  * depth:正則匹配深度,默認為12,即匹配成功次數達到此值以后終止匹配,主要考慮是性能。如果一個超長的日志,我們不應該全部替換,否則可能引入性能問題。 
  23.  * maxLength:單條message的最大長度(不計算throwable),超長則截取,並在message尾部追加終止符。 
  24.  * <p> 
  25.  * 考慮到擴展性,用戶仍然可以直接配置pattern,此時regex、policy、depth等option則不生效。但是maxLength會一致生效。 
  26.  * 格式樣例: 
  27.  * %d{yyyy-MM-dd/HH:mm:ss.SSS}|IP_OR_HOSTNAME|REQUEST_ID|REQUEST_SEQ|^_^| 
  28.  * SYS_K1:%property{SYS_K1}|SYS_K2:%property{SYS_K2}|MDC_K1:%X{MDC_K1:--}|MDC_K2:%X{MDC_K2:--}|^_^| 
  29.  * [%t] %-5level %logger{50} %line - %m{o1,o2,o3,o4}%n 
  30.  * 格式中domain1是必選,而且限定無法擴展 
  31.  * domain2根據配置文件指定的system properties和mdcKeys動態拼接,K-V結構,便於解析;可以為空。 
  32.  * domain3是常規message部分,其中%m攜帶options,此后Converter可以獲取這些參數。 
  33.  **/  
  34. public class CommonPatternLayoutEncoder extends PatternLayoutEncoder {  
  35.   
  36.   
  37.     protected static final String PATTERN_D1 = "%d'{'yyyy-MM-dd/HH:mm:ss.SSS'}'|{0}|%X'{'requestId:--'}'|%X'{'requestSeq:--'}'";  
  38.     protected static final String PATTERN_D2_S1 = "{0}:%property'{'{1}'}'";  
  39.     protected static final String PATTERN_D2_S2 = "{0}:%X'{'{1}:--'}'";  
  40.     protected static final String PATTERN_D3_S1 = "[%t] %-5level %logger{50} %line - ";  
  41.     //0:message最大長度(超出則截取),1:正則表達式,2:policy,3:查找深度(超過深度后停止正則匹配)  
  42.     protected static final String PATTERN_D3_S2 = "%m'{'{0},{1},{2},{3}'}'%n";  
  43.   
  44.     protected String mdcKeys;//來自MDC的key,多個key用逗號分隔。  
  45.   
  46.     protected String regex = "-";//匹配的正則表達式,如果此值為null或者"-",那么policy、deep參數都將無效  
  47.   
  48.     protected int maxLength = 2048;//單條消息的最大長度,主要是message  
  49.   
  50.     protected String policy = "replace";//如果匹配成功,字符串的策略。  
  51.   
  52.     protected int depth = 128;  
  53.   
  54.     protected boolean useDefaultRegex = true;  
  55.   
  56.     protected static final String DEFAULT_REGEX = "'((?<\\d)1[3-9]\\d{9}(?!\\d))'";//手機號,11位數字,並且前后位不再是數字。  
  57.     //系統參數,如果未指定,則使用default;  
  58.     protected String systemProperties;  
  59.   
  60.     protected static final String DEFAULT_SYSTEM_PROPERTIES = "project,profiles,cloudPlatform,clusterName";  
  61.   
  62.     @Override  
  63.     public void start() {  
  64.         if (getPattern() == null) {  
  65.             StringBuilder sb = new StringBuilder();  
  66.             String d1 = MessageFormat.format(PATTERN_D1, Utils.getHostName());  
  67.             sb.append(d1);  
  68.             sb.append(FIELD_DELIMITER)  
  69.                     .append(DOMAIN_DELIMITER)  
  70.                     .append(FIELD_DELIMITER);  
  71.             //拼裝系統參數,如果當前數據視圖不存在,則先set一個默認值  
  72.             if (systemProperties == null || systemProperties.isEmpty()) {  
  73.                 systemProperties = DEFAULT_SYSTEM_PROPERTIES;  
  74.             }  
  75.             //系統參數  
  76.             String[] properties = systemProperties.split(",");  
  77.             for (String property : properties) {  
  78.                 String value = Utils.getSystemProperty(property);  
  79.                 if (value == null) {  
  80.                     System.setProperty(property, "-");//初始化  
  81.                 }  
  82.                 sb.append(MessageFormat.format(PATTERN_D2_S1, property, property))  
  83.                         .append(FIELD_DELIMITER);  
  84.             }  
  85.   
  86.             //拼接MDC參數  
  87.             if (mdcKeys != null) {  
  88.                 String[] keys = mdcKeys.split(",");  
  89.                 for (String key : keys) {  
  90.                     sb.append(MessageFormat.format(PATTERN_D2_S2, key, key));  
  91.                     sb.append(FIELD_DELIMITER);  
  92.                 }  
  93.                 sb.append(DOMAIN_DELIMITER)  
  94.                         .append(FIELD_DELIMITER);  
  95.             }  
  96.             sb.append(PATTERN_D3_S1);  
  97.   
  98.             if (PolicyEnum.codeOf(policy) == null) {  
  99.                 policy = "-";  
  100.             }  
  101.   
  102.             if (maxLength < 0 || maxLength > 10240) {  
  103.                 maxLength = 2048;  
  104.             }  
  105.   
  106.             //如果設定了自定義regex,則優先生效;否則使用默認  
  107.             if (!regex.equalsIgnoreCase("-")) {  
  108.                 useDefaultRegex = false;  
  109.             }  
  110.             if (useDefaultRegex) {  
  111.                 regex = DEFAULT_REGEX;  
  112.             }  
  113.   
  114.             sb.append(MessageFormat.format(PATTERN_D3_S2, String.valueOf(maxLength), regex, policy, String.valueOf(depth)));  
  115.             setPattern(sb.toString());  
  116.         }  
  117.         super.start();  
  118.     }  
  119.   
  120.     public String getMdcKeys() {  
  121.         return mdcKeys;  
  122.     }  
  123.   
  124.     public void setMdcKeys(String mdcKeys) {  
  125.         this.mdcKeys = mdcKeys;  
  126.     }  
  127.   
  128.     public String getRegex() {  
  129.         return regex;  
  130.     }  
  131.   
  132.     public void setRegex(String regex) {  
  133.         this.regex = regex;  
  134.     }  
  135.   
  136.     public int getMaxLength() {  
  137.         return maxLength;  
  138.     }  
  139.   
  140.     public void setMaxLength(int maxLength) {  
  141.         this.maxLength = maxLength;  
  142.     }  
  143.   
  144.     public String getPolicy() {  
  145.         return policy;  
  146.     }  
  147.   
  148.     public void setPolicy(String policy) {  
  149.         this.policy = policy;  
  150.     }  
  151.   
  152.     public int getDepth() {  
  153.         return depth;  
  154.     }  
  155.   
  156.     public void setDepth(int depth) {  
  157.         this.depth = depth;  
  158.     }  
  159.   
  160.     public Boolean getUseDefaultRegex() {  
  161.         return useDefaultRegex;  
  162.     }  
  163.   
  164.     public boolean isUseDefaultRegex() {  
  165.         return useDefaultRegex;  
  166.     }  
  167.   
  168.     public void setUseDefaultRegex(boolean useDefaultRegex) {  
  169.         this.useDefaultRegex = useDefaultRegex;  
  170.     }  
  171.   
  172.     @Override  
  173.     public String getPattern() {  
  174.         return super.getPattern();  
  175.     }  
  176.   
  177.     @Override  
  178.     public void setPattern(String pattern) {  
  179.         super.setPattern(pattern);  
  180.     }  
  181.   
  182.     public String getSystemProperties() {  
  183.         return systemProperties;  
  184.     }  
  185.   
  186.     public void setSystemProperties(String systemProperties) {  
  187.         this.systemProperties = systemProperties;  
  188.     }  
  189.   
  190.   
  191. }  

 

    1)日志格式部分,僅供參考。

 

    2)MDC參數聲明格式為:%X{key},如果上下文中key不存在,則打印"";我們通過使用“:-”來聲明其默認值,比如%X{key:--}表示如果key不存在則將打印“-”

 

    3)根據logback的規定,option參數列表需要聲明在某個字段中,並配合<conversionRule>才能生效,以本文為例,我們主要對message進行整形,所以option參數聲明在%m上,其格式為:

    “%m{o1,o2...}”,多個option之間以“,”分割。然后o1,o2的字面值,將可以在Converter中獲取。簡單來說,你需要將參數傳遞給Converter時,這些參數必須以option方式聲明在某個字段上,否則沒法做。

    特別注意,如果option參數中如果包含“{”、“}”時,必須將option參數使用''包括。比如%m{2048,'\\d{11}','replace','128'},為了便於理解,建議所有的option參數都使用''逐個包含。

 

    此外,如果你對日志格式中,還需要使用系統參數(System Property),可以使用“%property{key}”來聲明,有個問題,就是如果這些系統參數不是通過“java -jar -Dkey=value”設置的,而是在運行時通過System.setProperty(key,value)設置的,這些系統參數在logback初始化時是無法獲得的,因為logback初始化結束后才會執行application程序;你可以在Encoder的start方法中先設定為這些系統參數設定一個默認值,以免日志打印是出現大量null。

 

    4)MessageFormat格式化字符串時,字符串中如果包含“{”、“}”特殊字符,也需要將這兩個字符使用''包含,比如:

     MessageFormat.format("展示一下'{'{0}'}'格式化的效果。","hello") 

        輸出>>

    "展示一下{hello}格式化效果。"

 

    5)useDefaultRegex:是否使用默認表達式,即手機號數字(連續11位數字,且后位不再跟進數字)。

 

    6)regex:我們也允許用戶自定義表達式。此時需要將useDefaultRegex設定為false才能生效。

    7)maxLength:默認值為2048,即message的最大長度超過此值后將會被截取,可配置。

    8)policy:對於regex匹配成功的字符串,如何處理。(處理規則,參見下文ComplexMessageConverter)

        A)drop:直接拋棄,將message重置為一個“終止符號”。比如:

        “我的手機號為18611001100”

            將會被整形為:

        “><”

        B)replace:替換,將敏感信息除去前三、后四位字符之外的其他字符用“*”替換,也是默認策略。比如:

        “我的手機號為18611001100”

            將會被整形為:

        “我的手機號為186****1100”

        C)erase:參數,將匹配成功的字符串,全部替換為等長度的“*”,比如:

        “我的手機號為18611001100”

            將會被整形為:

        “我的手機號為***********”

 

    9)depth:匹配深度,即message中,最多匹配成功的次數,超過之后將會終止匹配,主要考慮性能,默認值為128。假如message中有200個手機號,那么匹配和替換到128個之后,將會終止操作,剩余的手機號將不會再替換。

 

    10)mdcKeys:指定pattern拼接時,需要植入的mdc參數列表,比如mdcKeys="name,address",那么在pattern中將會包含:

    “name:%X{name:--}|address:%X{address:--}”

 

    其實大家主要關注的是option部分,Encoder的主要作用就是拼接一個pattern大概樣例:

Java代碼   收藏代碼
  1. 格式樣例:  
  2. %d{yyyy-MM-dd/HH:mm:ss.SSS}|IP_OR_HOSTNAME|REQUEST_ID|REQUEST_SEQ|^_^|  
  3.     SYS_K1:%property{SYS_K1}|SYS_K2:%property{SYS_K2}|MDC_K1:%X{MDC_K1:--}|MDC_K2:%X{MDC_K2:--}|^_^|  
  4.     [%t] %-5level %logger{50} %line - %m{2048,'(\\d{11})','replace',128}  
  5.   
  6. 格式中domain1是必選,而且限定無法擴展  
  7. domain2根據配置文件指定的system properties和mdcKeys動態拼接,K-V結構,便於解析;可以為空。  
  8. domain3是常規message部分,其中%m攜帶options,此后Converter可以獲取這些參數。  

   

 

    2、ComplexMessageConverter.java

Java代碼   收藏代碼
  1. package ch.qos.logback.classic.pattern;  
  2.   
  3. import ch.qos.logback.classic.PolicyEnum;  
  4. import ch.qos.logback.classic.spi.ILoggingEvent;  
  5.   
  6. import java.util.List;  
  7. import java.util.regex.Matcher;  
  8. import java.util.regex.Pattern;  
  9.   
  10. /** 
  11.  * @author liuguanqing 
  12.  * created 2018/6/22 下午8:01 
  13.  * <p> 
  14.  * 日志格式轉換器,會為每個appender創建一個實例,所以在配置層面需要考慮兼容。 
  15.  * 主要目的是,根據配置的regex來匹配message,對於匹配成功的字符串進行替換操作,並返回修正后的message。 
  16.  **/  
  17. public class ComplexMessageConverter extends MessageConverter {  
  18.   
  19.     protected String regex = "-";  
  20.     protected int depth = 0;  
  21.     protected String policy = "-";  
  22.     protected int maxLength = 2048;  
  23.     private ReplaceMatcher replaceMatcher = null;  
  24.   
  25.     @Override  
  26.     public void start() {  
  27.         List<String> options = getOptionList();  
  28.         //如果存在參數選項,則提取  
  29.         if (options != null && options.size() == 4) {  
  30.             maxLength = Integer.valueOf(options.get(0));  
  31.             regex = options.get(1);  
  32.             policy = options.get(2);  
  33.             depth = Integer.valueOf(options.get(3));  
  34.   
  35.             if ((regex != null && !regex.equals("-"))  
  36.                     && (PolicyEnum.codeOf(policy) != null)  
  37.                     && depth > 0) {  
  38.                 replaceMatcher = new ReplaceMatcher();  
  39.             }  
  40.         }  
  41.         super.start();  
  42.     }  
  43.   
  44.     @Override  
  45.     public String convert(ILoggingEvent event) {  
  46.         String source = event.getFormattedMessage();  
  47.         if (source == null || source.isEmpty()) {  
  48.             return source;  
  49.         }  
  50.         //復雜處理的原因:盡量少的字符串轉換、空間重建、字符移動。共享一個builder  
  51.         if (source.length() > maxLength || replaceMatcher != null) {  
  52.             StringBuilder sb = null;  
  53.             //如果超長截取  
  54.             if (source.length() > maxLength) {  
  55.                 sb = new StringBuilder(maxLength + 6);  
  56.                 sb.append(source.substring(0, maxLength))  
  57.                         .append("❮❮❮");//后面增加三個終止符  
  58.             }  
  59.             //如果啟動了matcher  
  60.             if (replaceMatcher != null) {  
  61.                 //如果沒有超過maxLength  
  62.                 if (sb == null) {  
  63.                     sb = new StringBuilder(source);  
  64.                 }  
  65.                 return replaceMatcher.execute(sb, policy);  
  66.             }  
  67.   
  68.             return sb.toString();  
  69.         }  
  70.   
  71.         return source;  
  72.     }  
  73.   
  74.     class ReplaceMatcher {  
  75.         Pattern pattern;  
  76.   
  77.         ReplaceMatcher() {  
  78.             pattern = Pattern.compile(regex);  
  79.         }  
  80.   
  81.         String execute(StringBuilder source, String policy) {  
  82.   
  83.             Matcher matcher = pattern.matcher(source);  
  84.   
  85.             int i = 0;  
  86.             while (matcher.find() && (i < depth)) {  
  87.                 i++;  
  88.                 int start = matcher.start();  
  89.                 int end = matcher.end();  
  90.                 if (start < 0 || end < 0) {  
  91.                     break;  
  92.                 }  
  93.                 String group = matcher.group();  
  94.                 switch (policy) {  
  95.                     case "drop":  
  96.                         return "❯❮";//只要匹配,立即返回  
  97.                     case "replace":  
  98.                         source.replace(start, end, facade(group, true));  
  99.                         break;  
  100.                     case "erase":  
  101.                     default:  
  102.                         source.replace(start, end, facade(group, false));  
  103.                         break;  
  104.   
  105.                 }  
  106.             }  
  107.             return source.toString();  
  108.         }  
  109.   
  110.     }  
  111.   
  112.     /** 
  113.      * 混淆,但是不能改變字符串的長度 
  114.      * 
  115.      * @param source 
  116.      * @param included 
  117.      * @return 
  118.      */  
  119.     public static String facade(String source, boolean included) {  
  120.         int length = source.length();  
  121.         StringBuilder sb = new StringBuilder();  
  122.         //長度超過11的,保留前三、后四,中間全部*替換  
  123.         //低於11位或者included=false,全部*替換  
  124.         if (length >= 11) {  
  125.             if (included) {  
  126.                 sb.append(source.substring(0, 3));  
  127.             } else {  
  128.                 sb.append("***");  
  129.             }  
  130.             sb.append(repeat('*', length - 7));  
  131.             if (included) {  
  132.                 sb.append(source.substring(length - 4));  
  133.             } else {  
  134.                 sb.append(repeat('*', 4));  
  135.             }  
  136.         } else {  
  137.             sb.append(repeat('*', length));  
  138.         }  
  139.   
  140.         return sb.toString();  
  141.     }  
  142.   
  143.     private static String repeat(char t, int times) {  
  144.         char[] r = new char[times];  
  145.         for (int i = 0; i < times; i++) {  
  146.             r[i] = t;  
  147.         }  
  148.         return new String(r);  
  149.     }  
  150. }  

 

   此類主要是從CommonPatternLayoutEncoder聲明的options(即regix、maxLength、policy、depth)並初始化一個Matcher,針對message進行匹配和替換。正則比較消耗CPU,此外還要認真設計,避免在message處理過程中,新建太多的字符串,否則會大量消耗內存;我們在處理時,盡可能確保主message只有一個,replace時不改變message的長度,可以避免因為重建String導致一些空間浪費

 

    自所以Converter能夠發揮作用,離不開<conversionRule>,參看下文的配置樣例。不過還需要注意,每個Appender都會根據<conversionRule>創建一個Converter實例,所以Converter設計時注意代碼兼容。

 

    3、logback.xml配置樣例

Java代碼   收藏代碼
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <configuration>  
  3.   
  4.     ...  
  5.   
  6.     <conversionRule conversionWord="m" converterClass="ch.qos.logback.classic.pattern.ComplexMessageConverter"/>  
  7.   
  8.     <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">  
  9.         <filter class="ch.qos.logback.classic.filter.ThresholdFilter">  
  10.             <level>INFO</level>  
  11.         </filter>  
  12.         <file>你的日志文件名</file>  
  13.         <Append>true</Append>  
  14.         <prudent>false</prudent>  
  15.         <encoder class="ch.qos.logback.classic.encoder.CommonPatternLayoutEncoder">  
  16.             <useDefaultRegex>true</useDefaultRegex>  
  17.             <policy>replace</policy>  
  18.             <maxLength>2048</maxLength>  
  19.         </encoder>  
  20.         <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">  
  21.             <FileNamePattern>你的日志名.%d{yyyy-MM-dd}.%i</FileNamePattern>  
  22.             <maxFileSize>64MB</maxFileSize>  
  23.             <maxHistory>7</maxHistory>  
  24.             <totalSizeCap>6GB</totalSizeCap>  
  25.         </rollingPolicy>  
  26.     </appender>  
  27.   
  28.     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">  
  29.         <encoder class="ch.qos.logback.classic.encoder.ConsolePatternLayoutEncoder"/>  
  30.     </appender>  
  31.   
  32.     ...  
  33. </configuration>  

    

    <conversionRule>節點中的“conversionWord='m'”,其中m就是對應pattern中的“%m”,可以從“%m”獲取options列表。

    因為CommonPatternLayoutEncoder中已經限定了pattern的格式,所以我們再logback.xml中也不需要再顯示的聲明pattern參數,基於此可以限定業務日志的格式保持統一。當然如果有特殊情況需要自定義,仍然可以使用<pattern>來聲明以覆蓋默認格式。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM