SpringBoot2集成Log4j2并实现日志脱敏

最近在搭建springCloud项目,正好这个废了我点时间,也就记录下来,防止下次使用,也为了方便别人

首先是pom.xml配置:

删除springBoot自己的logback,导入log4j2

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>

然后创建咱们的log4j2.xml配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<!-- 根节点Configuration有两个属性:status和monitorinterval,有两个子节点:Appenders和Loggers(表明可以定义多个Appender和Logger) -->
<!-- status:这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,会看到log4j2内部各种详细输出-->
<!-- monitorInterval:用于指定log4j自动重新配置的监测间隔时间,单位是s,最小是5s;Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
<Configuration status="info">
	<Appenders>
		<!-- SYSTEM_OUT是输出到统一的输出流,没有指定日志文件 -->
		<Console name="Console" target="SYSTEM_OUT">
			<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}]%p[%t]%c{1}|%m%n" />
		</Console>

		<!-- RollingFile节点用来定义超过指定大小自动删除旧的创建新的的Appender.   name:指定Appender的名字.   fileName:指定输出日志的目的文件带全路径的文件名.   filePattern:指定新建日志文件的名称格式. -->
		<!-- 详细日志 -->
		<RollingRandomAccessFile name="DetailRollingFile" fileName="log/consumer/consumer_detail.log" filePattern="log/consumer/consumer_detail.log.%d{yyyyMMddHH}" immediateFlush="true">
			<!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
			<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
			<!-- PatternLayout:输出格式,不设置默认为:%m%n. -->
			<Log4jEncodeLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}]%p[%t]%c{1}|%m%n" charset="UTF-8"/>
			<!-- Policies:指定滚动日志的策略. -->
			<Policies>
				<!-- TimeBasedTriggeringPolicy:Policies子节点,基于时间的滚动策略,interval属性用来指定多久滚动一次,默认是1 hour。modulate=true用来调整时间:比如现在是早上3am,interval是4,那么第一次滚动是在4am,接着是8am,12am...而不是7am. -->
				<TimeBasedTriggeringPolicy interval="1"/>
			</Policies>
		</RollingRandomAccessFile>
		<!-- 简要日志 -->
		<RollingRandomAccessFile name="MpspRollingFile" fileName="log/consumer/consumer_mpsp.log" filePattern="log/consumer/consumer_mpsp.log.%d{yyyyMMddHH}">
			<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
			<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}]%m%n"/>
			<Policies>
				<TimeBasedTriggeringPolicy interval="1"/>
			</Policies>
			<!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件,用来指定同一个文件夹下最多有几个日志文件时开始删除最旧的,创建新的(通过max属性) -->
			<!--<DefaultRolloverStrategy max="200"/>-->
		</RollingRandomAccessFile>
		<!-- 性能监控日志 -->
		<RollingRandomAccessFile name="ssRollingLog" fileName="log/consumer/ss.log" filePattern="log/consumer/ss.log.%d{yyyyMMddHH}">
			<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
			<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}]%m%n"/>
			<Policies>
				<TimeBasedTriggeringPolicy interval="1"/>
			</Policies>
		</RollingRandomAccessFile>
		<!-- 关键日志 -->
		<RollingRandomAccessFile name="mpspRollingLog" fileName="log/consumer/mpsp.log" filePattern="log/consumer/mpsp.log.%d{yyyyMMddHH}">
			<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
			<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}]%m%n"/>
			<Policies>
				<TimeBasedTriggeringPolicy interval="1"/>
			</Policies>
		</RollingRandomAccessFile>
	</Appenders>

	<!--定义logger,只有定义了logger并引入的appender,appender才会生效-->
	<Loggers>
		<!-- additivity的值如果为false的话,就不会在控制台上输出或者为该Logger再增加一个输出源Consloe -->
		<Logger name="payGateMpspLog" level="INFO" additivity="false">
			<AppenderRef ref="MpspRollingFile" />
		</Logger>
		<Logger name="mpspLog" level="INFO" additivity="false">
			<AppenderRef ref="mpspRollingLog" />
		</Logger>
		<Logger name="ssLog" level="INFO" additivity="false">
			<AppenderRef ref="ssRollingLog" />
		</Logger>
		<Logger name="payGateBusiLog" level="INFO" additivity="false">
			<AppenderRef ref="DetailRollingFile" />
		</Logger>
		<Logger name="com" level="INFO" additivity="false">
			<AppenderRef ref="DetailRollingFile" />
		</Logger>
		<Logger name="com" level="INFO" additivity="false">
			<AppenderRef ref="DetailRollingFile" />
		</Logger>
		<Root level="info">
			<AppenderRef ref="Console" />
		</Root>
	</Loggers>
</Configuration>

这里要注意!!!

由于我的是要实现日志脱敏的,所以,详细日志的打印格式,我是自己定义的,并不是   PatternLayout

如果只想要实现集成,则需要改正。

如果对xml配置项不太懂得,请参考这篇文章:https://www.cnblogs.com/hafiz/p/6170702.html    写的挺不错的

接下来是实现日志脱敏,说白了,就是匹配正则表达式,将value值更换为********

这里是实现类代码:

package com.umpay.utils;

import java.nio.charset.Charset;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.umpay.utils.StringUtils;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Node;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.layout.AbstractStringLayout;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * log4j1.x重写log4j的PatternLayout,实现日志信息中敏感信息的编码或加密
 * log4j2.x重写log4j的AbstractStringLayout,实现日志信息中敏感信息的编码或加密
 */
@Plugin(name = "Log4jEncodeLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
public class Log4jEncodeLayout extends AbstractStringLayout {

	private PatternLayout patternLayout;

	protected Log4jEncodeLayout(Charset charset, String pattern) {
		super(charset);
		patternLayout = PatternLayout.newBuilder().withPattern(pattern).build();
		init();
	}

	private static final Logger log = LoggerFactory.getLogger(Log4jEncodeLayout.class);

	/** 要匹配的正则表达式(身份证/银行卡) */
	private Pattern patternChar;
	/** 要匹配的正则表达式(姓名) */
	private Pattern namepatternChar;

	private Pattern cvvpatternChar;
	private int keys_length_char;// 要匹配关键字的数目
	private int keys_cvv_length_char;// 要匹配关键字的数目
	private String[] charKeys;
	private String[] cvvcharKeys;

	private void init() {
//		String str_char_keys = YamlConfigurerUtil.getStrYmlVal("umfWeChatCore.logLayout", "");
		String str_char_keys = StringUtils.trim(YmlUtils.getCommonYml("umfWeChatCore.logLayout"));
		StringBuffer charSbf = new StringBuffer();
		StringBuffer cvvSbf = new StringBuffer();
		String cvv_char_keys = "";

		if (StringUtils.isEmpty(str_char_keys)) {// 没有配置log4j.char.keys,不需要替换*
			return;
		}

		// 2、区分脱敏字段配置规则
		// 把带[]的脱敏字段放入到cvvSbf中,其他脱敏字段放入到charSbf中
		String[] charKeysStrs = str_char_keys.split(",");// 读取配置中要加密的key
		for (int i = 0; i < charKeysStrs.length; i++) {
			if (isOptional(charKeysStrs[i])) {
				// 带[]的脱敏字段
				cvvSbf.append(charKeysStrs[i].substring(1, charKeysStrs[i].length() - 1) + ",");
			} else {
				charSbf.append(charKeysStrs[i] + ",");
			}
		}
		str_char_keys = charSbf.toString();
		cvv_char_keys = cvvSbf.toString();
		// 去掉拼转好的字符串最后一个逗号“,”
		if (StringUtils.isNotEmpty(str_char_keys) && str_char_keys.length() > 0) {
			str_char_keys = str_char_keys.substring(0, str_char_keys.length() - 1);
		}
		if (StringUtils.isNotEmpty(cvv_char_keys) && cvv_char_keys.length() > 0) {
			cvv_char_keys = cvv_char_keys.substring(0, cvv_char_keys.length() - 1);
		}

		// 3、组织需要脱敏字段的正则表达式
		if (str_char_keys.length() > 0) {
			charKeys = str_char_keys.split(",");// 读取配置中要加密的key
			StringBuffer sbRegChar = new StringBuffer();
			sbRegChar.append("(");
			for (int i = 0; i < charKeys.length; i++) {
				sbRegChar.append("(");
				sbRegChar.append(charKeys[i]);
				sbRegChar.append(")");
				if (i < charKeys.length - 1) {
					sbRegChar.append("|");
				}
			}
			StringBuffer namesbRegChar = new StringBuffer(sbRegChar);
			namesbRegChar.append(")(=|:|:|>)([\u4E00-\u9FA5\u00B7]{1,16})(;|,|\\||\\}|]|$|&|<)");
			// 由于(\\s*\\w+={0,2}) key:value 中value中结尾包括==(base64导致,所以修改该写法)
			sbRegChar.append(")(=|:|:|>)(\\s*\\w+={0,2})(;|,|\\||\\}|]|$|&|<)");

			namepatternChar = Pattern.compile(namesbRegChar.toString());// 编译正则表达式
			patternChar = Pattern.compile(sbRegChar.toString());// 编译正则表达式
			keys_length_char = charKeys.length;
		}

		if (cvv_char_keys.length() > 0) {
			cvvcharKeys = cvv_char_keys.split(",");// 读取配置中要加密的key
			StringBuffer cvvRegChar = new StringBuffer();
			cvvRegChar.append("(");
			for (int i = 0; i < cvvcharKeys.length; i++) {
				cvvRegChar.append("(");
				cvvRegChar.append(cvvcharKeys[i]);
				cvvRegChar.append(")");
				if (i < cvvcharKeys.length - 1) {
					cvvRegChar.append("|");
				}
			}
			cvvRegChar.append(")(=|:|:|>)(.*?)(;|,|\\||\\}|]|$|&|<)");
			cvvpatternChar = Pattern.compile(cvvRegChar.toString());// 编译正则表达式
			keys_cvv_length_char = cvvcharKeys.length;
		}
	}

	public static void main(String[] args) {
		Log4jEncodeLayout log = new Log4jEncodeLayout(null, "");
	}

	/**
	 * 对银行卡以及身份证号脱敏处理 1.判断配置文件中是否已经配置需要脱敏字段 2.判断敏感信息内容是否需要处理 2.1 处理银行卡和身份证敏感信息
	 * 2.2 处理用户姓名敏感信息 2.3 处理需要不展示字段(例如:cvv=123展示为cvv=)
	 *
	 * @param @param msg
	 * @param @return
	 * @return String
	 * @throws
	 */
	private String dimChar(String msg) {
		try {
			// 1.判断配置文件中是否已经配置需要脱敏字段
			if (keys_length_char <= 0 && keys_cvv_length_char <= 0) {
				return msg;
			}

			boolean nameFlag = false;
			Matcher match = null;
			StringBuffer sbMsg = new StringBuffer();
			if (patternChar != null) {
				match = patternChar.matcher(msg);
				// 处理要打印的日志信息
				// 2.判断敏感信息内容是否需要处理
				while (match.find()) {
					// 2.1 处理银行卡和身份证敏感信息
					nameFlag = true;
					// group(1)匹配key,group(keys_length+2)匹配(=|:|:),group(keys_length+3)匹配value,group(keys_length+4)匹配(;|,|\\||]|\\)|$)
					match.appendReplacement(sbMsg, match.group(1)
							+ match.group(keys_length_char + 2)
							+ replaceChar(match.group(keys_length_char + 3))
							+ match.group(keys_length_char + 4));
				}
				match.appendTail(sbMsg);// 增加最后一个匹配项后面的值
			}

			if (namepatternChar != null) {
				// 2.2 处理用户姓名敏感信息
				if (nameFlag) {
					match = namepatternChar.matcher(sbMsg);
				} else {
					match = namepatternChar.matcher(msg);
				}
				sbMsg = new StringBuffer();
				while (match.find()) {
					nameFlag = true;
					// group(1)匹配key,group(keys_length+2)匹配(=|:|:),group(keys_length+3)匹配value,group(keys_length+4)匹配(;|,|\\||]|\\)|$)
					match.appendReplacement(sbMsg,
							match.group(1)
									+ match.group(keys_length_char + 2)
									+ replaceNameChar(match.group(keys_length_char + 3))
									+ match.group(keys_length_char + 4));
				}
				match.appendTail(sbMsg);// 增加最后一个匹配项后面的值
			}

			if (cvvpatternChar != null) {
				if (nameFlag) {
					match = cvvpatternChar.matcher(sbMsg);
				} else {
					match = cvvpatternChar.matcher(msg);
				}
				sbMsg = new StringBuffer();
				// 2.3 处理需要不展示字段(例如:cvv=123展示为cvv=)
				while (match.find()) {
					// group(1)匹配key,group(keys_length+2)匹配(=|:|:),group(keys_length+3)匹配value,group(keys_length+4)匹配(;|,|\\||]|\\)|$)
					match.appendReplacement(
							sbMsg,
							match.group(1)
									+ match.group(keys_cvv_length_char + 2)
									+ replaceCvvChar(match.group(keys_cvv_length_char + 3))
									+ match.group(keys_cvv_length_char + 4));
				}
				match.appendTail(sbMsg);// 增加最后一个匹配项后面的值
			}
			return sbMsg.toString();
		} catch (Exception e) {
			// 如果跑出异常为了不影响流程,直接返回原信息
			log.error(e.getMessage(), e);
		}
		return msg;
	}

	private boolean isOptional(String key) {
		return key.startsWith("[") && key.endsWith("]");
	}

	/**
	 * 日志关键字用* 替换 规则: value <= 6 替换为****** 6 > value <= 12 后四位保留,其他为* value > 12
	 * 前6位保留 中间* 后4位保留
	 *
	 * @param value
	 * @return
	 */
	private static String replaceChar(String value) {

		if (StringUtils.isEmpty(value)) {
			return "";
		}
		if (value.length() <= 6) { // value <= 6 替换为******
			return lStr("", '*', value.length());
		} else if (value.length() > 6 && value.length() <= 12) { // 后四位保留,其他为*
			int valLen = value.length();
			String replaceHeadStr = value.substring(value.length() - 4);
			return replaceHeadStr = lStr(replaceHeadStr, '*', valLen);
		} else if (value.length() > 12) { // 前6位保留 中间* 后4位保留
			String replaceHeadStr = value.substring(0, 6);
			String replaceTailStr = value.substring(value.length() - 4);
			String dimStr = lStr("", '*', value.length() - 10);
			return replaceHeadStr + dimStr + replaceTailStr;
		} else {
			return value;
		}
	}

	/**
	 * 日志姓名关键字替换 保留第一个汉字,后边用*号表示(例如:张三--》张*)
	 *
	 * @param @param value
	 * @param @return
	 * @return String
	 * @throws
	 */
	private static String replaceNameChar(String value) {

		if (StringUtils.isEmpty(value)) {
			return "";
		}
		String replaceHeadStr = value.substring(0, 1);
		return replaceHeadStr + lStr("", '*', value.length() - 1);
	}

	/**
	 * 日志cvv关键字替换 直接输出空(例如:1608--》)
	 *
	 * @param @param value
	 * @param @return
	 * @return String
	 * @throws
	 */
	private static String replaceCvvChar(String value) {
		return "";
	}

	/*
	 * 左补长char
	 */
	public static String lStr(String s, char ch, int width) {
		if (s.length() < width) { // 需要前面补'0'
			while (s.length() < width)
				s = ch + s;
		} else { // 需要将高位丢弃
			s = s.substring(s.length() - width, s.length());
		}
		return s;
	}

	@Override
	public String toSerializable(LogEvent event) {
		String message = patternLayout.toSerializable(event);
		return dimChar(message);
	}

	@PluginFactory
	public static Layout createLayout(
			@PluginAttribute(value = "pattern") final String pattern,
			// LOG4J2-783 use platform default by default, so do not specify
			// defaultString for charset
			@PluginAttribute(value = "charset") final Charset charset) {
		return new Log4jEncodeLayout(charset, pattern);
	}

}

这个类,也就是我上面说过的,那个自定义的,至此,实现完成,但是还差个问题就是如何获取配置项的值。

package com.umpay.utils;

import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

import java.util.Properties;

/**
 * 读取配置文件地址
 */
public class YmlUtils {
    private static String PROPERTY_NAME = "application.yml";

    public static Object getCommonYml(Object key){
        Resource resource = new ClassPathResource(PROPERTY_NAME);
        Properties properties = null;
        try {
            YamlPropertiesFactoryBean yamlFactory = new YamlPropertiesFactoryBean();
            yamlFactory.setResources(resource);
            properties =  yamlFactory.getObject();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        return properties.get(key);
    }
}


 

如果还有哪里写的不对,也有请大佬们批评指正。

 

 

  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值