logback - 自定义日志脱敏组件,一种不错的脱敏方案,。

完全借鉴了https://blog.csdn.net/qq_40885085/article/details/113385261

应该是extends logback的实际Appender,然后,在这个自定义Appender中调用脱敏工具,脱敏工具需要有个配置,也就需要一个logback-desensitize.xml,这个和工具类的指定。读取这个配置类的工具为YmlUtils。感觉也应该可以用该方法来实现log4j2的。


log-desensitization项目

项目目录图片:

logback - 自定义日志脱敏组件,一种不错的脱敏方案_Boo

DesensitizationAppender.java

import ch.qos.logback.classic.spi.LoggingEvent;
import top.imddy.logdesensitization.utils.DesensitizationUtils;

import java.lang.reflect.Field;

/**
 * @ClassName DesensitizationAppender
 * @Description 脱敏类 - 将日志进行脱敏
 * @Author 柳成荫
 * @Date 2021/1/9
 */
public class DesensitizationAppender {
    /**
     * LoggingEvent的属性 - message
     * 格式化前的日志信息,如log.info("your name : {}", "柳成荫")
     * message就是"your name : {}"
     */
    private static final String MESSAGE = "message";
    /**
     * LoggingEvent的属性 - formattedMessage
     * 格式化后的日志信息,如log.info("your name : {}", "柳成荫")
     * formattedMessage就是"your name : 柳成荫"
     */
    private static final String FORMATTED_MESSAGE = "formattedMessage";

    public void operation(LoggingEvent event) {
        // event.getArgumentArray() - 获取日志中的参数数组
        // 如:log.info("your name : {}, your id : {}", "柳成荫", 11)
        // event.getArgumentArray() => ["柳成荫",11]
        if (event.getArgumentArray() != null) {
            // 获取格式化后的Message
            String eventFormattedMessage = event.getFormattedMessage();
            DesensitizationUtils util = new DesensitizationUtils();
            // 获取替换后的日志信息
            String changeMessage = util.customChange(eventFormattedMessage);
            if (!(null == changeMessage || "".equals(changeMessage))) {
                try {
                    // 利用反射的方式,将替换后的日志设置到原event对象中去
                    Class<? extends LoggingEvent> eventClass = event.getClass();
                    // 保险起见,将message和formattedMessage都替换了
                    Field message = eventClass.getDeclaredField(MESSAGE);
                    message.setAccessible(true);
                    message.set(event, changeMessage);
                    Field formattedMessage = eventClass.getDeclaredField(FORMATTED_MESSAGE);
                    formattedMessage.setAccessible(true);
                    formattedMessage.set(event, changeMessage);
                } catch (IllegalAccessException | NoSuchFieldException e) {
                    e.printStackTrace();
                }
            }

        }
    }
}

LcyConsoleAppender.java

package top.imddy.logdesensitization.logbackadvice;

import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.ConsoleAppender;

/**
 * @ClassName ConsoleAppenderDS
 * @Description
 * @Author 柳成荫
 * @Date 2021/1/9
 */
public class LcyConsoleAppender extends ConsoleAppender {

    @Override
    protected void subAppend(Object event) {
        DesensitizationAppender appender = new DesensitizationAppender();
        appender.operation((LoggingEvent)event);
        super.subAppend(event);
    }
}

LcyFileAppender.java

package top.imddy.logdesensitization.logbackadvice;

import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.FileAppender;

/**
 * @ClassName FileAppenderDS
 * @Description
 * @Author 柳成荫
 * @Date 2021/1/9
 */
public class LcyFileAppender extends FileAppender {

    @Override
    protected void subAppend(Object event) {
        DesensitizationAppender appender = new DesensitizationAppender();
        appender.operation((LoggingEvent) event);
        super.subAppend(event);
    }
}

LcyRollingFileAppender.java

package top.imddy.logdesensitization.logbackadvice;

import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.rolling.RollingFileAppender;

/**
 * @ClassName RollingFileAppenderDS
 * @Description
 * @Author 柳成荫
 * @Date 2021/1/9
 */
public class LcyRollingFileAppender extends RollingFileAppender {

    @Override
    protected void subAppend(Object event) {
        DesensitizationAppender appender = new DesensitizationAppender();
        appender.operation((LoggingEvent)event);
        super.subAppend(event);
    }
}

DesensitizationUtils.java

package top.imddy.logdesensitization.utils;


import org.springframework.util.CollectionUtils;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @ClassName DesensitizationUtil
 * @Description 脱敏工具类
 * @Author 柳成荫
 * @Date 2021/1/9
 */
public class DesensitizationUtils {
    /**
     * 正则匹配模式 - 该正则表达式第三个()可能无法匹配以某些特殊符号开头和结尾的(如果像密码这种字段,前后如果有很多特殊字段,则无法匹配,建议密码直接加密,无需脱敏)
     */
    public static final Pattern REGEX_PATTERN = Pattern.compile("\\s*([\"]?[\\w]+[\"]?)(\\s*[:=]+[^\\u4e00-\\u9fa5@,.*{\\[\\w]*\\s*)([\\u4e00-\\u9fa5_\\-@.\\w]+)[\\W&&[^\\-@.]]?\\s*");
    // 该正则表达式第三个()可以匹配以某些特殊字符开头和结尾的,但是对于日志来说,处理也很麻烦
    // public static final Pattern REGEX_PATTERN = Pattern.compile("\\s*([\"]?[\\w]+[\"]?)(\\s*[::=><]+\\s*)([\\S]+[\\u4e00-\\u9fa5\\w]+[\\S]+)[\\W&&[^\\-@.]]?\\s*");

    /**
     * 匹配非数字
     */
    public static final Pattern REGEX_NUM = Pattern.compile("[^0-9]");

    /**
     * 是否开启脱敏
     */
    public static Boolean openFlag = false;
    /**
     * 是否忽略key的大小写
     */
    public static Boolean ignoreFlag = true;
    /**
     * 作为ignoreFlag初始的标记
     */
    private static Boolean initIgnoreFlag = false;
    /**
     * 作为openFlag初始化的标记
     */
    private static Boolean initOpenFlag = false;
    /**
     * 所有key:value配置匹配对
     */
    public static Map<String, Object> allPattern;
    /**
     * key为全小写的allPattern - pattern和patterns
     */
    public static Map<String, Object> lowerCaseAllPattern;
    /**
     * 手机
     */
    public static final String PHONE = "phone";
    /**
     * 邮箱
     */
    public static final String EMAIL = "email";
    /**
     * 身份证
     */
    public static final String IDENTITY = "identity";
    /**
     * 自定义
     */
    public static final String OTHER = "other";
    /**
     * 密码
     */
    public static final String PASSWORD = "password";

    /**
     * 将event对象的formattedMessage脱敏
     *
     * @param eventFormattedMessage LoggingEvent的formattedMessage属性
     * @return 脱敏后的日志信息
     */
    public String customChange(String eventFormattedMessage) {
        try {
            // 原始信息 - 格式化后的
            String originalMessage = eventFormattedMessage;
            boolean flag = false;
            // 获取Yml配置文件内容 - Map格式
            Map<String, Object> patternMap = YmlUtils.patternMap;
            if (!CollectionUtils.isEmpty(patternMap)) {
                // 如果没有开启脱敏,返回"",则不会做脱敏操作
                if (!this.checkOpen(patternMap)) {
                    return "";
                }
                // 获取一个原始Message的正则匹配器
                Matcher regexMatcher = REGEX_PATTERN.matcher(eventFormattedMessage);
                // 如果部分匹配(一个对象/JSON字符串/Map/List<对象/Map>等会有多个匹配),就根据分组来获取key和value
                while (regexMatcher.find()) {
                    // group(1)就是key,group(2)就是分隔符(如:和=),group(3)就是value
                    try {
                        // 获取key - 将引号替换掉,去掉两边空格(JSON字符串去引号)
                        String key = regexMatcher.group(1).replaceAll("\"", "").trim();
                        // 获取原始Value
                        String originalValue = regexMatcher.group(3);
                        // 获取Key对应规则
                        Object keyPatternValue = this.getKeyIgnoreCase(key);
                        if (null != keyPatternValue && null != originalValue && !"null".equals(originalValue)) {
                            // 将原始Value - 引号替换掉,去掉两边空格(JSON字符串去引号)
                            String value = originalValue.replaceAll("\"", "").trim();
                            if (!"null".equals(value) || value.equalsIgnoreCase(key)) {
                                String patternVales = getMultiplePattern(keyPatternValue, value);
                                if ("".equals(patternVales)) {
                                    // 不符规则/没有规则的不能影响其他符合规则的
                                    continue;
                                }
                                patternVales = patternVales.replaceAll(" ", "");
                                if(PASSWORD.equalsIgnoreCase(patternVales)){
                                    String origin = regexMatcher.group(1) + regexMatcher.group(2) + regexMatcher.group(3);
                                    originalMessage = originalMessage.replace(origin, regexMatcher.group(1) + regexMatcher.group(2) + "******");
                                    flag=true;
                                    // 密码级别的,直接替换为全*,继续下一轮匹配
                                    continue;
                                }
                                // 原始的规则(完整)
                                String originalPatternValues = patternVales;
                                // 判断这个规则是否带括号,带括号的需要把括号拿出来 - 核心规则
                                String filterData = this.getBracketPattern(patternVales);
                                if (!"".equals(filterData)) {
                                    patternVales = filterData;
                                }
                                // 以逗号分割
                                String[] split = patternVales.split(",");
                                value = getReplaceValue(value, patternVales, split, originalPatternValues);
                                if (value != null && !"".equals(value)) {
                                    flag = true;
                                    String origin = regexMatcher.group(1) + regexMatcher.group(2) + regexMatcher.group(3);
                                    originalMessage = originalMessage.replace(origin, regexMatcher.group(1) + regexMatcher.group(2) + value);
                                }
                            }
                        }
                    } catch (Exception e) {
                        // 捕获到异常,直接返回结果(空字符串) - 这个异常可能发生的场景:同时开启控制台和输出文件的时候
                        // 当控制台进行一次脱敏之后,文件的再去脱敏,是对脱敏后的message脱敏,则正则匹配会出现错误
                        // 比如123456789@.com 脱敏后:123***456789@qq.com,正则匹配到123,这个123去substring的时候会出错
                        return "";
                    }
                }
            }
            return flag ? originalMessage : "";
        } catch (Exception e) {
            return "";
        }
    }


    /**
     * 获取替换后的value
     * @param value value
     * @param patternVales 核心规则
     * @param split 分割
     * @param originalPatternValues 原始规则
     * @return
     */
    private String getReplaceValue(String value, String patternVales, String[] split, String originalPatternValues) {
        if (split.length >= 2 && !"".equals(patternVales)) {
            String append = "";
            String start = REGEX_NUM.matcher(split[0]).replaceAll("");
            String end = REGEX_NUM.matcher(split[1]).replaceAll("");
            int startSub = Integer.parseInt(start) - 1;
            int endSub = Integer.parseInt(end) - 1;
            // 脱敏起点/结尾符下标
            int index;
            String flagSub;
            int indexOf;
            int newValueL;
            String newValue;
            // 脱敏结尾
            if (originalPatternValues.contains(">")) {
                // 获取>的下标
                index = originalPatternValues.indexOf(">");
                // 获取标志符号
                flagSub = originalPatternValues.substring(0, index);
                // 获取标志符号的下标
                indexOf = value.indexOf(flagSub);
                // 获取标志符号前面数据
                newValue = value.substring(0, indexOf);
                // 获取数据的长度
                newValueL = newValue.length();
                // 获取标识符及后面的数据
                append = value.substring(indexOf);
                value = this.dataDesensitization(Math.max(startSub, 0), endSub >= 0 ? (endSub <= newValueL ? endSub : newValueL - 1) : 0, newValue) + append;
            } else if (originalPatternValues.contains("<")) {
                // 脱敏起点
                index = originalPatternValues.indexOf("<");
                flagSub = originalPatternValues.substring(0, index);
                indexOf = value.indexOf(flagSub);
                newValue = value.substring(indexOf + 1);
                newValueL = newValue.length();
                append = value.substring(0, indexOf + 1);
                value = append + this.dataDesensitization(Math.max(startSub, 0), endSub >= 0 ? (endSub <= newValueL ? endSub : newValueL - 1) : 0, newValue);
            } else if (originalPatternValues.contains(",")) {
                newValueL = value.length();
                value = this.dataDesensitization(Math.max(startSub, 0), endSub >= 0 ? (endSub <= newValueL ? endSub : newValueL - 1) : 0, value);
            }
        } else if (!"".equals(patternVales)) {
            int beforeIndexOf = patternVales.indexOf("*");
            int last = patternVales.length() - patternVales.lastIndexOf("*");
            int lastIndexOf = value.length() - last;
            value = this.dataDesensitization(beforeIndexOf, lastIndexOf, value);
        }
        return value;
    }

    /**
     * 根据key获取对应的规则(也许是Map,也许是String)
     *
     * @param key key
     * @return key对应的规则(也许是Map , 也许是String)
     */
    private Object getKeyIgnoreCase(String key) {
        // 获取所有pattern
        if (CollectionUtils.isEmpty(allPattern)) {
            allPattern = YmlUtils.getAllPattern();
        }
        // 作为ignoreFlag初始化的标记,第一次ignoreFlag需要从Yml中获取是否开启
        // 后面就不用去Yml里获取了
        if (!initIgnoreFlag) {
            initIgnoreFlag = true;
            // 仅在第一次会去获取,无论true还是false(默认是开启忽略大小写)
            ignoreFlag = YmlUtils.getIgnore();
            if (ignoreFlag) {
                // 如果忽略大小写,就去获取一份key小写化的allPattern
                lowerCaseAllPattern = this.transformUpperCase(allPattern);
            }
        }
        // 只有忽略大小写的时候,才去从lowerCaseAllPattern里获取
        if (ignoreFlag) {
            return lowerCaseAllPattern.get(key.toLowerCase());
        } else {
            // 否则从原始的pattern中取
            return allPattern.get(key);
        }
    }



    /**
     * 将pattern的key值全部转换为小写
     *
     * @param pattern pattern
     * @return 转换后的pattern
     */
    public Map<String, Object> transformUpperCase(Map<String, Object> pattern) {
        Map<String, Object> resultMap = new HashMap();
        if (pattern != null && !pattern.isEmpty()) {
            // 获取Key的Set集合
            Set<String> keySet = pattern.keySet();
            Iterator<String> iterator = keySet.iterator();
            // 黄线强迫症,用for代替while
            for (; iterator.hasNext(); ) {
                String key = iterator.next();
                // 把key转换为小写字符串
                String newKey = key.toLowerCase();
                // 重新放入
                resultMap.put(newKey, pattern.get(key));
            }
        }
        return resultMap;
    }


    /**
     * 获取规则字符串
     *
     * @param patternVale 规则
     * @param newValue    key对应的值 - 如 name:liuchengyin  这个参数就是liuchengyn
     * @return 规则的字符串
     */
    private String getMultiplePattern(Object patternVale, String newValue) {
        if (patternVale instanceof String) {
            // 如果规则是String类型,直接转换为String类型返回
            return (String) patternVale;
        } else if (patternVale instanceof Map) {
            // 获取规则 - Map类型(不推荐,有风险)
            return this.getPatternByMap((Map<String, Object>) patternVale, newValue);
        } else { // 获取规则 - List<Map>类型,一个Key可能有多种匹配规则
            if (patternVale instanceof List) {
                List<Map<String, Object>> list = (List<Map<String, Object>>) patternVale;
                if (!CollectionUtils.isEmpty(list)) {
                    Iterator<Map<String, Object>> iterator = list.iterator();
                    // 遍历每一种规则
                    for (; iterator.hasNext(); ) {
                        Map<String, Object> map = iterator.next();
                        String patternValue = this.getPatternByMap(map, newValue);
                        // 如果是空的,表示没匹配上该规则,去匹配下一个规则
                        if (!"".equals(patternValue)) {
                            return patternValue;
                        }
                    }
                }
            }
            return "";
        }
    }


    /**
     * 获取规则
     *
     * @param map   规则
     * @param value key对应的值 - 如 name:liuchengyin  这个参数就是liuchengyn
     * @return
     */
    private String getPatternByMap(Map<String, Object> map, String value) {
        if (CollectionUtils.isEmpty(map)) {
            // 为空就是无规则
            return "";
        } else {
            // 获取匹配规则 - 自定义规则(正则)
            Object customRegexObj = map.get("customRegex");
            // 获取脱敏方式
            Object positionObj = map.get("position");
            // 获取匹配规则 - 自定义规则(正则)
            String customRegex = "";
            // position必须有
            String position = "";
            if (customRegexObj instanceof String) {
                customRegex = (String) customRegexObj;
            }
            if (positionObj instanceof String) {
                position = (String) positionObj;
            }
            // 如果日志中的值能够匹配,直接返回其对应的规则
            if (!"".equals(customRegex) && value.matches(customRegex)) {
                return position;
            } else {
                // 如果不能匹配到正则,就看他是不是内置规则
                Object defaultRegexObj = map.get("defaultRegex");
                String defaultRegex = "";
                if (defaultRegexObj instanceof String) {
                    defaultRegex = (String) defaultRegexObj;
                }
                // 这段代码写的多多少少感觉有点问题,可以写在一个if里,但是阿里检测代码的工具会警告
                if (!"".equals(defaultRegex)) {
                    if(IDENTITY.equals(defaultRegex) && isIdentity(value)){
                        return position;
                    }else if(EMAIL.equals(defaultRegex) && isEmail(value)){
                        return position;
                    }else if(PHONE.equals(defaultRegex) && isMobile(value)){
                        return position;
                    }else if(OTHER.equals(defaultRegex)){
                        return position;
                    }
                }
                return "";
            }
        }
    }


    /**
     * 获取规则 - 判断是否带括号,带括号则返回括号内数据
     * @param patternVales 规则
     * @return 规则
     */
    private String getBracketPattern(String patternVales) {
        // 是否存在括号
        if (patternVales.contains("(")) {
            int startCons = patternVales.indexOf("(");
            int endCons = patternVales.indexOf(")");
            patternVales = patternVales.substring(startCons + 1, endCons);
            return patternVales;
        } else {
            return "";
        }
    }

    public static boolean isEmail(String str) {
        return str.matches("^[\\w-]+@[\\w-]+(\\.[\\w-]+)+$");
    }

    public static boolean isIdentity(String str) {
        return str.matches("(^\\d{18}$)|(^\\d{15}$)");
    }

    public static boolean isMobile(String str) {
        return str.matches("^1[0-9]{10}$");
    }

    /**
     * 检查是否开启脱敏
     *
     * @param pattern Yml配置文件内容 - Map格式
     * @return 是否开启脱敏
     */
    private Boolean checkOpen(Map<String, Object> pattern) {
        // 作为openFlag初始化的标记,第一次openFlag需要从Yml中获取是否开启
        // 后面就不用去Yml里获取了
        if (!initOpenFlag) {
            initOpenFlag = true;
            // 仅在第一次会去获取
            openFlag = YmlUtils.getOpen();
        }
        // 第二次以后openFlag已经有值,无论true还是false(默认是未开启)
        return openFlag;
    }


    /**
     * 脱敏处理
     * @param start 脱敏开始下标
     * @param end 脱敏结束下标
     * @param value value
     * @return
     */
    public String dataDesensitization(int start, int end, String value) {
        char[] chars;
        int i;
        // 正常情况 - end在数组长度内
        if (start >= 0 && end + 1 <= value.length()) {
            chars = value.toCharArray();
            // 脱敏替换
            for (i = start; i < chars.length && i < end + 1; ++i) {
                chars[i] = '*';
            }
            return new String(chars);
        } else if (start >= 0 && end >= value.length()) {
            // 非正常情况 - end在数组长度外
            chars = value.toCharArray();
            for (i = start; i < chars.length; ++i) {
                chars[i] = '*';
            }
            return new String(chars);
        } else {
            // 不符要求,不脱敏
            return value;
        }
    }
}

YmlUtils.java

package top.imddy.logdesensitization.utils;

import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.springframework.util.CollectionUtils;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;

/**
 * @ClassName YmlUtils
 * @Description Yml配置文件操作相关
 * @Author 柳成荫
 * @Date 2021/1/9
 */
public class YmlUtils {
    /** 默认脱敏配置文件名 - 默认在resources目录下 */
    public static String PROPERTY_NAME = "logback-desensitize.yml";
    /** Key:pattern - 单规则 */
    public static final String PATTERN = "pattern";
    /** Key:patterns - 多规则 */
    public static final String PATTERNS = "patterns";
    /** Key:open - 是否开启脱敏 */
    public static final String OPEN_FLAG = "open";
    /** Key:ignore - 是否开启忽略大小写匹配 */
    public static final String IGNORE = "ignore";
    /** Key:脱敏配置文件头Key */
    public static final String YML_HEAD_KEY = "log-desensitize";
    /** key:patterns对应key下的规则Key */
    public static final String CUSTOM = "custom";
    /** Yml脱敏配置文件内容 - Map格式 */
    public static Map<String, Object> patternMap;
    public static final DumperOptions OPTIONS = new DumperOptions();


    static {
        OPTIONS.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
        patternMap = getYmlByName(PROPERTY_NAME);
    }

    /**
     * 获取Yml配置文件的内容 - 以Map的格式
     * @param fileName Yml配置文件名
     * @return 配置信息(Map格式)
     */
    private static Map<String, Object> getYmlByName(String fileName) {
        if (CollectionUtils.isEmpty(patternMap)) {
            Object fromYml = null;
            try {
                // 获取Yml配置文件的Map对象
                fromYml = getFromYml(fileName, YML_HEAD_KEY);
                // LinkedHashMap,如果不是Map类型(比如配置文件里只有log-desensitize=123456),直接返回patternMap本身
                if (fromYml instanceof Map) {
                    return (Map)fromYml;
                }
            } catch (Exception e) {
                return null;
            }
        }
        return patternMap;
    }

    /**
     * 通过key获取value从yml配置文件
     * @param fileName Yml文件名
     * @param key key
     * @return value或者map本身
     */
    public static Object getFromYml(String fileName, String key){
        // 创建一个Yaml对象
        Yaml yaml = new Yaml(OPTIONS);
        // 获得流
        InputStream inputStream = YmlUtils.class.getClassLoader().getResourceAsStream(fileName);
        HashMap<String, Object> map = (HashMap<String, Object>)yaml.loadAs(inputStream, HashMap.class);
        // 如果map内有值,直接返回key对应的Value,否则返回map本身
        return Objects.nonNull(map) && map.size() > 0 ? map.get(key) : map;
    }

    /**
     * 获取key为pattern的值
     * @return pattern对应的map,或者null(如pattern=123这种情况)
     */
    public static Map<String, Object> getPattern() {
        Object pattern = patternMap.get(PATTERN);
        if (pattern instanceof Map) {
            return (Map<String, Object>)pattern;
        } else {
            return null;
        }
    }

    /**
     * 获取所有pattern,含key为pattern,key为patterns
     * @return pattern
     */
    public static Map<String, Object> getAllPattern() {
        Map<String, Object> allPattern = new HashMap<String, Object>();
        Map<String, Object> pattern = getPattern();
        Map<String, Object> patterns = getPatterns();
        if (!CollectionUtils.isEmpty(patterns)) {
            allPattern.putAll(patterns);
        }
        // 注意:patterns中的key与pattern的key重复,patterns中的不生效(Map无重复Key)
        if (!CollectionUtils.isEmpty(pattern)) {
            allPattern.putAll(pattern);
        }
        return allPattern;
    }

    /**
     * 获取key为patterns的值
     * @return patterns对应的map,或者null(如patterns=123这种情况)
     */
    public static Map<String, Object> getPatterns() {
        Map<String, Object> map = new HashMap<String, Object>();
        Object patterns = patternMap.get(PATTERNS);
        // patterns下有多个key的时候(List)
        if (patterns instanceof List) {
            // 获取key为"patterns"的值(List<Map<String, Object>>)
            List<Map<String, Object>> list = (List<Map<String, Object>>)patterns;
            if (!CollectionUtils.isEmpty(list)) {
                Iterator<Map<String, Object>> iterator = list.iterator();
                // 黄线强迫症,用for代替while
                for (;iterator.hasNext();){
                    Map<String, Object> maps = (Map<String, Object>)iterator.next();
                    assembleMap(map, maps);
                }
                return map;
            }
        }
        // patterns只有一个key的时候,且非List
        if (patterns instanceof Map) {
            assembleMap(map, (Map<String, Object>)patterns);
            return map;
        } else {
            return null;
        }
    }

    /**
     * 将patterns中每个key对应的规则按<key,规则>的方式放入map
     * @param map map
     * @param patterns patterns
     */
    private static void assembleMap(Map<String, Object> map, Map<String, Object> patterns) {
        // 获取patterns里key值为"key"的值(脱敏关键字)
        Object key = patterns.get("key");
        if (key instanceof String) {
            // 清除空格
            String keyWords = ((String) key).replace(" ", "");
            // 以逗号分隔出一个key数组
            String[] keyArr = keyWords.split(",");
            for(String keyStr : keyArr){
                map.put(keyStr, patterns.get(CUSTOM));
            }
        }
    }

    /**
     * 是否开启脱敏,默认不开启
     * @return 是否开启脱敏
     */
    public static Boolean getOpen() {
        Object flag = patternMap.get(OPEN_FLAG);
        return flag instanceof Boolean ? (Boolean)flag : false;
    }

    /**
     * 是否忽略大小写匹配,默认开启
     * @return 是否忽略大小写匹配
     */
    public static Boolean getIgnore() {
        Object flag = patternMap.get(IGNORE);
        return flag instanceof Boolean ? (Boolean)flag : true;
    }
}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>top.imddy</groupId>
    <artifactId>log-desensitization</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.6.15</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.8</version>
        </dependency>
    </dependencies>

</project>


log-test项目(用于测试)

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>top.imddy</groupId>
    <artifactId>log-test</artifactId>
    <version>1.0-SNAPSHOT</version>


    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>


    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.6.15</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


    <dependencies>
        <dependency>
            <groupId>top.imddy</groupId>
            <artifactId>log-desensitization</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

    </dependencies>


    <build>
        <finalName>log-test</finalName>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.6.15</version>
                <configuration>
                    <fork>true</fork>
                    <includeSystemScope>true</includeSystemScope>
                    <!--fork : 如果没有该项配置,肯呢个devtools不会起作用,即应用不会restart -->
                    <!--这里写上main方法所在类的路径-->
                    <mainClass>top.imddy.logtest.Application</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
                <configuration>
                    <skip>true</skip>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>3.3.1</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                    <!-- 过滤后缀为pem、pfx的证书文件 -->
                    <nonFilteredFileExtensions>
                        <nonFilteredFileExtension>p12</nonFilteredFileExtension>
                        <nonFilteredFileExtension>cer</nonFilteredFileExtension>
                        <nonFilteredFileExtension>pem</nonFilteredFileExtension>
                        <nonFilteredFileExtension>pfx</nonFilteredFileExtension>
                    </nonFilteredFileExtensions>
                </configuration>
            </plugin>
        </plugins>
    </build>


</project>

Test01Controller.java

package top.imddy.logtest.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;

@RestController
@RequestMapping("/test01")
public class Test01Controller {
    private static final Logger log = LoggerFactory.getLogger(Test01Controller.class);


    @GetMapping("/info")
    public String info() {
        log.info("localMobile:{},localMobile:{}", "023-55327459", "123456");//规范语法 -推荐,格外推荐使用=分割。
        log.info("your email:{},your phone:{}", "123456789@q9.com", "15310763497");
        log.info("your email={},your cellphone={}", "123456789@q9.com", "15310763497");
        log.info("email:{},mobile:}", "123456789@qq.com", "023-55327459");
        log.info("identity:{}", "123456789012345");
        // Map类型
        HashMap<String, String> map = new HashMap<>();
        map.put("phone", "15310763497");
        map.put("email", "123456789@qq.com");
        log.info("one map={}", map);
        log.info("one json={}", "{" + "\t\"email\":\"123456789@gq.com\"," + "\t\"phone\":\"15310764682\"" + "}");// 对象类型
        log.info("student:{}", new Student("柳成荫", "15372746384", "530365199703153648")); // 对于需要完全密文的
        log.info("password:{}", "189498asd6489s");
        //非规范 -但也能用 - 不推荐
        log.info(" mobile :{}.email:{}", 123456, "123456789@qq.com");
        log.info(" mobile :{}email:{}", 123456, "123456789@qq.com");
        log.info(" mobile :{}: email:{}", 123456, "123456789@qg.com");
        return "柳成荫";
    }



}

logback-spring.xml

2、替换日志文件配置类(logback.xml)
日志打印方式都只需要替换成脱敏的类即可,如果你的业务不需要,则无需替换。
①ConsoleAppender - 控制台脱敏
// 原类
ch.qos.logback.core.ConsoleAppender
// 替换类
pers.liuchengyin.logbackadvice.LcyConsoleAppender

②RollingFileAppender - 滚动文件
// 原类
ch.qos.logback.core.rolling.RollingFileAppender
// 替换类
pers.liuchengyin.logbackadvice.LcyRollingFileAppender

③FileAppender - 文件
// 原类
ch.qos.logback.core.FileAppender
// 替换类
pers.liuchengyin.logbackadvice.LcyFileAppender
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>

	<property name="logback.logdir" value="/logs/logtest" />
	<property name="logback.appname" value="logtest"/>

	<appender name="consoleLog"
		class="top.imddy.logdesensitization.logbackadvice.LcyConsoleAppender">
		<layout class="ch.qos.logback.classic.PatternLayout">
			<pattern>
				%date{yyyy-MM-dd HH:mm:ss.SSS} %-5level[%thread]%logger{56}.%method:%L -%msg%n
			</pattern>
		</layout>
	</appender>

	<appender name="fileInfoLog"
		class="top.imddy.logdesensitization.logbackadvice.LcyRollingFileAppender">
		<filter class="ch.qos.logback.classic.filter.LevelFilter">
			<level>ERROR</level>
			<onMatch>DENY</onMatch>
			<onMismatch>ACCEPT</onMismatch>
		</filter>
		<encoder>
			<pattern>
				%date{yyyy-MM-dd HH:mm:ss.SSS} %-5level[%thread]%logger{56}.%method:%L -%msg%n
			</pattern>
		</encoder>
		<!-- 滚动策略 -->
		<rollingPolicy
			class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
			<!-- 路径 -->
			<fileNamePattern>${logback.logdir}/info.${logback.appname}.%d.log</fileNamePattern>
			<MaxHistory>7</MaxHistory>
		</rollingPolicy>
	</appender>
	
		<appender name="fileDebugLog"
		class="top.imddy.logdesensitization.logbackadvice.LcyRollingFileAppender">
		<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
			<level>DEBUG</level>
		</filter>
		<encoder>
			<pattern>
				%date{yyyy-MM-dd HH:mm:ss.SSS} %-5level[%thread]%logger{56}.%method:%L -%msg%n
			</pattern>
		</encoder>

		<!-- 设置滚动策略 -->
		<rollingPolicy
			class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
			<!-- 路径 -->
			<fileNamePattern>${logback.logdir}/debug.${logback.appname}.%d.log</fileNamePattern>
			<MaxHistory>7</MaxHistory>
		</rollingPolicy>
	</appender>
	
	<appender name="fileWarnLog"
		class="top.imddy.logdesensitization.logbackadvice.LcyRollingFileAppender">
		<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
			<level>WARN</level>
		</filter>
		<encoder>
			<pattern>
				%date{yyyy-MM-dd HH:mm:ss.SSS} %-5level[%thread]%logger{56}.%method:%L -%msg%n
			</pattern>
		</encoder>

		<!-- 设置滚动策略 -->
		<rollingPolicy
			class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
			<!-- 路径 -->
			<fileNamePattern>${logback.logdir}/warn.${logback.appname}.%d.log</fileNamePattern>
			<MaxHistory>7</MaxHistory>
		</rollingPolicy>
	</appender>

	<appender name="fileErrorLog"
		class="top.imddy.logdesensitization.logbackadvice.LcyRollingFileAppender">
		<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
			<level>ERROR</level>
		</filter>
		<encoder>
			<pattern>
				%date{yyyy-MM-dd HH:mm:ss.SSS} %-5level[%thread]%logger{56}.%method:%L -%msg%n
			</pattern>
		</encoder>

		<!-- 设置滚动策略 -->
		<rollingPolicy
			class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
			<!-- 路径 -->
			<fileNamePattern>${logback.logdir}/error.${logback.appname}.%d.log</fileNamePattern>
			<MaxHistory>7</MaxHistory>
		</rollingPolicy>
	</appender>
	
	<root level="INFO">
		<appender-ref ref="consoleLog" />
		<appender-ref ref="fileInfoLog" />
		<appender-ref ref="fileDebugLog" />
		<appender-ref ref="fileWarnLog" />
		<appender-ref ref="fileErrorLog" />
	</root>
</configuration>

logback-desensitize.yml

# 日志脱敏
log-desensitize:
  # 是否忽略大小写匹配,默认为true
  ignore: true
  # 是否开启脱敏,默认为false
  open: true
  # pattern下的key/value为固定脱敏规则
  pattern:
    # 邮箱 - @前第4-7位脱敏
    email: "@>(4,7)"
    # qq邮箱 - @后1-3位脱敏
    qqemail: "@<(1,3)"
    # 姓名 - 姓脱敏,如*杰伦
    name: 1,1
    # 密码 - 所有需要完全脱敏的都可以使用内置的password
    password: password
  patterns:
    # 身份证号,key后面的字段都可以匹配以下规则(用逗号分隔)
    - key: identity,idcard
      # 定义规则的标识
      custom:
        # defaultRegex表示使用组件内置的规则:identity表示身份证号 - 内置的18/15位
        - defaultRegex: identity
          position: 9,13
        # 内置的other表示如果其他规则都无法匹配到,则按该规则处理
        - defaultRegex: other
          position: 9,10
    # 电话号码,key后面的字段都可以匹配以下规则(用逗号分隔)
    - key: phone,cellphone,mobile
      custom:
        # 手机号 - 内置的11位手机匹配规则
        - defaultRegex: phone
          position: 4,7
        # 自定义正则匹配表达式:座机号(带区号,号码七位|八位)
        - customRegex: "^0[0-9]{2,3}-[0-9]{7,8}"
          # -后面的1-4位脱敏
          position: "-<(1,4)"
        # 自定义正则匹配表达式:座机号(不带区号)
        - customRegex: "^[0-9]{7,8}"
          position: 3,5
        # 内置的other表示如果其他规则都无法匹配到,则按该规则处理
        - defaultRegex: other
          position: 1,3
    # 这种方式不太推荐 - 一旦匹配不上,就不会脱敏
    - key: localMobile
      custom:
        customRegex: "^0[0-9]{2,3}-[0-9]{7,8}"
        position: 1,3

测试结果

logback - 自定义日志脱敏组件,一种不错的脱敏方案_字符串_02

logback - 自定义日志脱敏组件,一种不错的脱敏方案_字符串_03