zoukankan      html  css  js  c++  java
  • 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>来声明以覆盖默认格式。

  • 相关阅读:
    HNUSTOJ-1675 Morse Code(DFS+字典序搜索)
    HNUSTOJ-1638 遍地桔子(贪心)
    HNUSTOJ-1521 塔防游戏
    HNUSTOJ-1565 Vampire Numbers(暴力打表)
    HDUSTOJ-1559 Vive la Difference!(简单题)
    HDUSTOJ-1558 Flooring Tiles(反素数)
    HNUSTOJ-1600 BCD时钟
    胡雪岩04
    新概念4-24
    曾国藩家训02
  • 原文地址:https://www.cnblogs.com/lizhonghua34/p/12187920.html
Copyright © 2011-2022 走看看