Java基于logback MessageConverter实现日志脱敏方案分析

 更新时间:2024年10月31日 15:40:41   作者:zhibo_lv  
本文介绍了一种日志脱敏方案,即基于logbackMessageConverter和正则匹配的方法,该方法的优点是侵入性低,工作量少,只需修改xml配置文件,适用于老项目,感兴趣的朋友跟随小编一起看看吧

背景简介

日志脱敏 是常见的安全需求,最近公司也需要将这一块内容进行推进。看了一圈网上的案例,很少有既轻量又好用的轮子可以让我直接使用。我一直是反对过度设计的,而同样我认为轮子就应该是可以让人拿去直接用的。所以我准备分享两篇博客分别实现两种日志脱敏方案。

方案分析

  • logback MessageConverter + 正则匹配本篇博客主要介绍此方法
    • 优势
      • 侵入性低、工作量极少, 只需要修改xml配置文件,适合老项目
        • 劣势
          • 效率低,会对每一行日志都进行正则匹配检查,效率受日志长度影响,日志越长效率越低,影响日志吞吐量
          • 因基于正则匹配 存在错杀风险,部分内容难以准确识别
  • fastjson Filter + 注解 + 工具类下一篇博客介绍
    • 优势
      • 性能损耗低、效率高、扩展性强,精准脱敏,适合QPS较高日志吞吐量较大的项目。
    • 劣势
      • 侵入性较高,需对所有可能的情况进行脱敏判断
      • 存在漏杀风险,全靠开发控制

其实还有一种方案,基于 工具类+配置模式
优势是 工作量低(比注解模式低,比正则匹配模式高),灵活度高,性能也好。但是只适合那些新项目,如果是老项目大家命名不规范,就很难推动整改了。此处不进行扩展。详见:项目日志脱敏

logback MessageConverter + 正则匹配

流程图解

代码案例

正则匹配日志脱敏工具类

此工具类主要用于实现依据配置的正则匹配规则集,进行依次匹配。并提取敏感文本对其执行对应的脱敏策略。大家拿去用可以不做修改

package com.zhibo.log.format;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
 * @Author: Zhibo.lv
 * @Description: 正则匹配日志脱敏工具类
 **/
@Component
public class LogSensitiveUtils {
    // 脱敏日志最大长度,超出此长度的日志放弃脱敏,直接返回
    private static Integer SENSITIVE_LOG_MAX_LENGTH = 10000;
    /**
     * 日志脱敏 获取规则集进行依次匹配
     * @param content 明文日志文本
     * @return 脱敏后的日志文本
     */
    public static String filterSensitive(String content) {
        try {
            if (StringUtils.isNotBlank(content) && content.length() < SENSITIVE_LOG_MAX_LENGTH) {
                for (Map.Entry<String, List<Pattern>> entry : LogSensitiveConstants.SENSITIVE_SEQUENCE.entrySet()) {
                    content = filter(content, entry.getKey(), entry.getValue());
                }
            }
            return content;
        } catch (Exception e) {
            return content;
        }
    }
    /**
     *
     * @param content   需脱敏字符串
     * @param type      文本类型,依据类型可以做不同的脱敏方式
     * @param patterns  该方式下需匹配的正则
     * @return
     *
     */
    public static String filter(String content, String type, List<Pattern> patterns) {
        for (Pattern pattern : patterns) {
            Matcher matcher = pattern.matcher(content);
            StringBuffer sb = new StringBuffer();
            while (matcher.find()) {
                matcher.appendReplacement(sb, Matcher.quoteReplacement(baseSensitive(matcher.group(), type)));
            }
            matcher.appendTail(sb);
            content = sb.toString();
        }
        return content;
    }
    /**
     * 依据正则抓去的文本执行对应的脱敏策略
     * @param str 待脱敏的字符串
     * @return
     */
    private static String baseSensitive(String str, String type) {
        if (StringUtils.isBlank(str)) {
            return StringUtils.EMPTY;
        }
		//通过工厂获取对应类型的脱敏类执行脱敏方法
        return SensitiveStrategyBuiltInUtil.getStrategy(type).des(str);
    }
}

正则匹配日志脱敏常量

此工具类主要是进行配置需要脱敏的文本的正则。需要大家依据业务调整或新增

package com.zhibo.log.format;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Pattern;
/**
 * @Author: Zhibo
 * @Description: 正则匹配日志脱敏常量
 **/
public class LogSensitiveConstants {
    /**
     * 过滤先后顺序:身份证 -> 手机号
     * 顺序原因:避免部分业务需求出现可能同时满足多个正则规则的文本,大家可以优先提取更长的、更复杂的文本。后处理简单的
     */
    public static final Map<String,List<Pattern>> SENSITIVE_SEQUENCE = new TreeMap<String, List<Pattern>>();
    /**
     * 手机号匹配规则集,支持配置多个正则规则
     */
    public static final List<Pattern> SENSITIVE_PHONE_KEY = new ArrayList<Pattern>(1);
    /**
     * 身份证号码匹配规则集,支持配置多个正则规则
     */
    public static final List<Pattern> SENSITIVE_ID_NO_KEY = new ArrayList<Pattern>(1);
    /**
     * 手机号正则匹配,11位1开头数字
     * 瞻前顾后:校验符合要求的文本前后均不能为数字 避免误匹配
     */
    public static final String PHONE_REGEX = "(?<!\\d)[1][3-9][0-9]{9}(?!\\d)";
    /**
     * 身份证号正则匹配 18位数版本
     * 15位数的身份证号码暂不考虑,如果需要自行新增下方正则加入 SENSITIVE_ID_NO_KEY 中
     * (?<!\d)([1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3})(?!\d)
     * 瞻前顾后:校验符合要求的文本前后均不能为数字 避免误匹配
     */
    public static final String ID_NO_REGEX = "(?<!\\d)([1-9]\\d{5}[1-9]\\d{3}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}([0-9]|X))(?!\\d)";
    static {
        SENSITIVE_ID_NO_KEY.add(Pattern.compile(ID_NO_REGEX));
        SENSITIVE_PHONE_KEY.add(Pattern.compile(PHONE_REGEX));
    }
	// 脱敏替代字符
	public static final char STAR = '*';
	// 手机号类型脱敏替代字符
    public static final String PHONE_MASK = "****";
    /** 手机号码脱敏策略 */
    public static final String STRATEGY_PHONE = "strategyPhone";
    /** 身份证号码脱敏策略 */
    public static final String STRATEGY_ID_NO = "strategyIdNo";
    static {
    	//将每一个规则集绑定一个对应的类型
        SENSITIVE_SEQUENCE.put(STRATEGY_ID_NO, SENSITIVE_ID_NO_KEY);
        SENSITIVE_SEQUENCE.put(STRATEGY_PHONE, SENSITIVE_PHONE_KEY);
    }
    private LogSensitiveConstants() {
    }
}

脱敏策略代码

定义文本脱敏接口 IStrategy

package com.zhibo.log.sensitive.api;
/**
 * @Author: Zhibo
 * @Description: 脱敏策略
 */
public interface IStrategy {
    /**
     * 脱敏
     * @param original 原始内容
     * @return 脱敏后的字符串
     */
    String des(final Object original);
}

文本脱敏抽象类,进行通用实现 AbstractStringStrategy

package com.zhibo.log.sensitive.core.strategory;
import com.zhibo.log.sensitive.api.IStrategy;
import com.zhibo.log.format.LogSensitiveConstants;
import java.security.MessageDigest;
/**
 * @Author: zhibo
 * @Description: 抽象字符串策略,
 * 				支持在脱敏后的文本后面追加明文的MD5加密串,方便研发进行日志查询使用
 */
public abstract class AbstractStringStrategy implements IStrategy {
    /**
     * 获取掩码之前的长度
     * @param original 原始
     * @param chars 字符串
     * @return 结果
     */
    protected abstract int getBeforeMaskLen(Object original, char[] chars);
    /**
     * 获取掩码之后的长度
     * @param original 原始
     * @param chars 字符串
     * @return 结果
     */
    protected abstract int getAfterMaskLen(Object original, char[] chars);
    /**
     * 针对固定长度的加密直接返回脱敏字符串,避免StringBuilder循环拼接
     * @return 脱敏字符串
     *      如返回null 则通过 {@link AbstractStringStrategy#getBeforeMaskLen(Object, char[])} 与 {@link AbstractStringStrategy#getAfterMaskLen(Object, char[])}
     *      进行截取字符串
     */
    protected String getMask(){
        return null;
    }
    /**
     * 是否需要拼接MD5密文方便日志查询。
     * @return false : 不拼接(默认)
     *          true :  拼接密文 用于日志查询 格式  [MD5]
     */
    protected Boolean addMD5(){
        return false;
    }
    @Override
    public String des(Object original) {
        if(original == null) {
            return null;
        }
        String strValue = original.toString();
        char[] chars = strValue.toCharArray();
        int beforeMaskLen = getBeforeMaskLen(original, chars);
        int afterMaskLen = getAfterMaskLen(original, chars);
        //范围纠正
        int maxLen = chars.length;
        beforeMaskLen = Math.min(beforeMaskLen, maxLen);
        afterMaskLen = Math.min(afterMaskLen, maxLen);
        StringBuilder stringBuilder = new StringBuilder();
        //获取明文前缀
        if(beforeMaskLen > 0) {
            stringBuilder.append(chars, 0, beforeMaskLen);
        }
        //获取脱敏字符串
        String mask = getMask();
        if (null == mask){//如未指定脱敏字符串则按规则循环拼接
            // 中间使用掩码
            for(int i = beforeMaskLen; i < chars.length - afterMaskLen; i++) {
                stringBuilder.append(LogSensitiveConstants.STAR);
            }
        }else {
            stringBuilder.append(mask);
        }
        //获取明文后缀
        if(afterMaskLen > 0) {
            stringBuilder.append(chars, chars.length - afterMaskLen, afterMaskLen);
        }
        if (addMD5()){
            addMD5(strValue,stringBuilder);
        }
        return stringBuilder.toString();
    }
    // MD5加密
	private void addMD5(String originalString,StringBuilder stringBuilder) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(originalString.getBytes());
            byte[] digest = md.digest();
            stringBuilder.append("[");
            for (byte b : digest) {
                stringBuilder.append(String.format("%02x", b));
            }
            stringBuilder.append("]");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

身份证脱敏策略实现 StrategyIdNo

package com.zhibo.log.sensitive.core.strategory;
/**
 * @Author: Zhibo
 * @Description: 身份证号脱敏
 * 脱敏规则:保留前6 后4 位,其它由星号替换
 */
public class StrategyIdNo extends AbstractStringStrategy {
    @Override
    protected int getBeforeMaskLen(Object original, char[] chars) {
        return 6;
    }
    @Override
    protected int getAfterMaskLen(Object original, char[] chars) {
        return 4;
    }
}

手机号码脱敏策略实现 StrategyPhone

package com.zhibo.log.sensitive.core.strategory;
import com.zhibo.log.format.LogSensitiveConstants;
/**
 * @Author: zhibo
 * @Description: 手机号脱敏
 * 脱敏规则:186****8567[MD5]
 */
public class StrategyPhone extends AbstractStringStrategy {
    @Override
    protected int getBeforeMaskLen(Object original, char[] chars) {
        return 3;
    }
    @Override
    protected int getAfterMaskLen(Object original, char[] chars) {
        return 4;
    }
    @Override
    protected String getMask() {
        return LogSensitiveConstants.PHONE_MASK;
    }
    @Override
    protected Boolean addMD5(){
        return true;
    }
}

logback 消息转换器实现

最关键的方法来啦

package com.zhibo.log.format;
import ch.qos.logback.classic.pattern.MessageConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
/**
 * @Author: Zhibo
 * @Description: 日志脱敏转换器
 **/
public class SensitiveConverter extends MessageConverter {
    @Override
    public String convert(ILoggingEvent event){
        // 获取原始日志
        String requestLogMsg = super.convert(event);
        // 执行日志脱敏
        return LogSensitiveUtils.filterSensitive(requestLogMsg);
    }
    public SensitiveConverter() {
    }
}

自此我们的工具包也就完成了,业务系统需要使用此工具只需要修改resources目录下的logback.xml配置。

<!-- 新增或修改原有消息转换器为SensitiveConverter -->
<conversionRule conversionWord="msgToo" converterClass="com.zhibo.log.format.SensitiveConverter" />

并将文件输出日志的消息内容替换为指定消息转换器的 conversionWord

脱敏效果展示

有请提示

注:此方法对日志吞吐量存在影响,由于正则需要循环匹配整个日志文本,所以正则规则越多,日志文本越长,耗时越长。如您的应用程序对日志吞吐量要求较高且存在大量超长日志文本请压测后使用。

如配置了logback的异步打印,且设置了允许日志丢弃,在压测中可能出现因线程池与等待队列均被占满而导致日志丢失情况。下面是我的问题复盘:

logback日志异步打印配置如下

<appender name="ASYNC-FILE" class="ch.qos.logback.classic.AsyncAppender">
		<neverBlock>true</neverBlock><!-- 非阻塞方式运行 如队列满就开始丢弃日志 -->
		<queueSize>1024</queueSize><!-- 等待队列大小 -->
		<discardingThreshold>0</discardingThreshold><!-- 日志队列深度,配置0 队列满后丢弃最老的日志 -->
		<appender-ref ref="FILE"/>
</appender>

以上配置 为logback线程池工作配置,默认线程池 线程数为 10个,最大队列长度为1024个。
意味着如果日志产生的速度超过10个线程工作处理日志的速度,则无法处理的日志会被写入BlockingQueue 队列,当队列满了之后就会导致日志丢失的情况。

到此这篇关于Java基于logback MessageConverter实现日志脱敏的文章就介绍到这了,更多相关java logback MessageConverter日志脱敏内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • shenyu怎么处理sign鉴权前置到网关

    shenyu怎么处理sign鉴权前置到网关

    这篇文章主要为大家介绍了shenyu怎么处理sign鉴权前置到网关方法详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • Java读取xml文件的五种方式

    Java读取xml文件的五种方式

    在编写与 XML 数据交互的现代软件应用时,有效地读取和解析 XML 文件是至关重要的,本文旨在探讨 Java 中处理 XML 文件的五种主要方法:DOM、SAX、StAX、JAXB 和 JDOM,我们将详细介绍每种方法的工作原理、典型用途以及如何在 Java 程序中实现它们
    2024-05-05
  • 用dom4j生成xml,去掉xml头的方法

    用dom4j生成xml,去掉xml头的方法

    今天小编就为大家分享一篇用dom4j生成xml,去掉xml头的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-07-07
  • SpringBoot内部外部配置文件加载顺序解析

    SpringBoot内部外部配置文件加载顺序解析

    这篇文章主要介绍了SpringBoot内部外部配置文件加载顺序解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-07-07
  • Java使用dom4j实现对xml简单的增删改查操作示例

    Java使用dom4j实现对xml简单的增删改查操作示例

    这篇文章主要介绍了Java使用dom4j实现对xml简单的增删改查操作,结合实例形式详细分析了Java使用dom4j实现对xml简单的增删改查基本操作技巧与相关注意事项,需要的朋友可以参考下
    2020-05-05
  • 一文带你了解SpringBoot中常用注解的原理和使用

    一文带你了解SpringBoot中常用注解的原理和使用

    这篇文章主要介绍了一文带你了解SpringBoot中常用注解的原理和使用
    2022-11-11
  • Spring实战之Bean销毁之前的行为操作示例

    Spring实战之Bean销毁之前的行为操作示例

    这篇文章主要介绍了Spring实战之Bean销毁之前的行为操作,结合实例形式分析了spring在bean销毁之前的行为相关设置与使用技巧,需要的朋友可以参考下
    2019-11-11
  • Java骚操作之CountDownLatch代码详解

    Java骚操作之CountDownLatch代码详解

    这篇文章主要介绍了Java骚操作之CountDownLatch代码详解,本文通过实例图文相结合给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-02-02
  • redis redisson 限流器的实例(RRateLimiter)

    redis redisson 限流器的实例(RRateLimiter)

    这篇文章主要介绍了redis redisson 限流器的实例(RRateLimiter),具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07
  • SpringBoot集成swagger的实例代码

    SpringBoot集成swagger的实例代码

    Swagger 是一款RESTFUL接口的文档在线自动生成+功能测试功能软件,这篇文章主要介绍了SpringBoot集成swagger,需要的朋友可以参考下
    2017-12-12

最新评论