SpringBoots3.5.7版本整合log4j2,配置自定义日志插件用于处理关键字脱敏后,控制台日志打印了两遍,一遍是自定义格式,一遍是默认格式,找了半天也没找到问题在哪里,求解决方案
相关maven依赖如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
log4j2.xml配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="INFO" monitorInterval="30">
<!--<Configuration status="WARN" monitorInterval="30"> -->
<properties>
<property name="LOG_HOME">./service-logs</property>
</properties>
<Appenders>
<!--*********************控制台日志***********************-->
<Console name="consoleAppender" target="SYSTEM_OUT">
<!--设置日志格式及颜色-->
<!-- <PatternLayout-->
<!-- pattern="%d %p %m%n %style{%d{ISO8601}}{bright,green} %highlight{%-5level} [%style{%t}{bright,blue}] %style{%C{}}{bright,yellow}: %msg%n%style{%throwable}{red}"-->
<!-- disableAnsi="false" noConsoleNoAnsi="false"/>-->
<CustomPatternLayout
pattern="%d %p %m%n %style{%d{ISO8601}}{bright,green} %highlight{%-5level} [%style{%t}{bright,blue}] %style{%C{}}{bright,yellow}: %msg%n%style{%throwable}{red}"
disableAnsi="false" noConsoleNoAnsi="false" charset="UTF-8"/>
</Console>
<!--*********************文件日志***********************-->
<!--debug级别日志-->
<RollingFile name="debugFileAppender"
fileName="${LOG_HOME}/debug.log"
filePattern="${LOG_HOME}/$${date:yyyy-MM-dd}/debug-%d{yyyy-MM-dd}-%i.log.gz">
<Filters>
<!--过滤掉info及更高级别日志-->
<ThresholdFilter level="info" onMatch="DENY" onMismatch="NEUTRAL"/>
</Filters>
<!--设置日志格式-->
<CustomPatternLayout
pattern="%d %p %m%n %style{%d{ISO8601}}{bright,green} %highlight{%-5level} [%style{%t}{bright,blue}] %style{%C{}}{bright,yellow}: %msg%n%style{%throwable}{red}"
disableAnsi="false" noConsoleNoAnsi="false" charset="UTF-8"/>
<Policies>
<!-- 设置日志文件切分参数 -->
<!--<OnStartupTriggeringPolicy/>-->
<!--设置日志基础文件大小,超过该大小就触发日志文件滚动更新-->
<SizeBasedTriggeringPolicy size="100 MB"/>
<!--设置日志文件滚动更新的时间,依赖于文件命名filePattern的设置-->
<TimeBasedTriggeringPolicy/>
</Policies>
<!--设置日志的文件个数上限,不设置默认为7个,超过大小后会被覆盖;依赖于filePattern中的%i-->
<DefaultRolloverStrategy max="100"/>
</RollingFile>
<!--info级别日志-->
<RollingFile name="infoFileAppender"
fileName="${LOG_HOME}/info.log"
filePattern="${LOG_HOME}/$${date:yyyy-MM-dd}/info-%d{yyyy-MM-dd}-%i.log.gz">
<Filters>
<!--过滤掉warn及更高级别日志-->
<ThresholdFilter level="warn" onMatch="DENY" onMismatch="NEUTRAL"/>
</Filters>
<!--设置日志格式-->
<CustomPatternLayout
pattern="%d %p %m%n %style{%d{ISO8601}}{bright,green} %highlight{%-5level} [%style{%t}{bright,blue}] %style{%C{}}{bright,yellow}: %msg%n%style{%throwable}{red}"
disableAnsi="false" noConsoleNoAnsi="false" charset="UTF-8"/>
<Policies>
<!-- 设置日志文件切分参数 -->
<!--<OnStartupTriggeringPolicy/>-->
<!--设置日志基础文件大小,超过该大小就触发日志文件滚动更新-->
<!-- <SizeBasedTriggeringPolicy size="100 MB"/>-->
<!--设置日志文件滚动更新的时间,依赖于文件命名filePattern的设置-->
<TimeBasedTriggeringPolicy interval="6" modulate="true"/>
</Policies>
<!--设置日志的文件个数上限,不设置默认为7个,超过大小后会被覆盖;依赖于filePattern中的%i-->
<!--<DefaultRolloverStrategy max="100"/>-->
</RollingFile>
<!--warn级别日志-->
<RollingFile name="warnFileAppender"
fileName="${LOG_HOME}/warn.log"
filePattern="${LOG_HOME}/$${date:yyyy-MM-dd}/warn-%d{yyyy-MM-dd}-%i.log.gz">
<Filters>
<!--过滤掉error及更高级别日志-->
<ThresholdFilter level="error" onMatch="DENY" onMismatch="NEUTRAL"/>
</Filters>
<!--设置日志格式-->
<CustomPatternLayout
pattern="%d %p %m%n %style{%d{ISO8601}}{bright,green} %highlight{%-5level} [%style{%t}{bright,blue}] %style{%C{}}{bright,yellow}: %msg%n%style{%throwable}{red}"
disableAnsi="false" noConsoleNoAnsi="false" charset="UTF-8"/>
<Policies>
<!-- 设置日志文件切分参数 -->
<!--<OnStartupTriggeringPolicy/>-->
<!--设置日志基础文件大小,超过该大小就触发日志文件滚动更新-->
<SizeBasedTriggeringPolicy size="100 MB"/>
<!--设置日志文件滚动更新的时间,依赖于文件命名filePattern的设置-->
<TimeBasedTriggeringPolicy/>
</Policies>
<!--设置日志的文件个数上限,不设置默认为7个,超过大小后会被覆盖;依赖于filePattern中的%i-->
<DefaultRolloverStrategy max="100"/>
</RollingFile>
<!--error及更高级别日志-->
<RollingFile name="errorFileAppender"
fileName="${LOG_HOME}/error.log"
filePattern="${LOG_HOME}/$${date:yyyy-MM-dd}/error-%d{yyyy-MM-dd}-%i.log.gz">
<!--设置日志格式-->
<CustomPatternLayout
pattern="%d %p %m%n %style{%d{ISO8601}}{bright,green} %highlight{%-5level} [%style{%t}{bright,blue}] %style{%C{}}{bright,yellow}: %msg%n%style{%throwable}{red}"
disableAnsi="false" noConsoleNoAnsi="false" charset="UTF-8"/>
<Policies>
<!-- 设置日志文件切分参数 -->
<!--<OnStartupTriggeringPolicy/>-->
<!--设置日志基础文件大小,超过该大小就触发日志文件滚动更新-->
<SizeBasedTriggeringPolicy size="100 MB"/>
<!--设置日志文件滚动更新的时间,依赖于文件命名filePattern的设置-->
<TimeBasedTriggeringPolicy/>
</Policies>
<!--设置日志的文件个数上限,不设置默认为7个,超过大小后会被覆盖;依赖于filePattern中的%i-->
<DefaultRolloverStrategy max="100"/>
</RollingFile>
</Appenders>
<Loggers>
<!--spring日志-->
<!-- <Logger name="org.springframework" level="info"/>-->
<!-- mybatis日志 -->
<Logger name="com.mybatis" level="info"/>
<Logger name="org.hibernate" level="info"/>
<Logger name="com.zaxxer.hikari" level="info"/>
<Logger name="org.quartz" level="info"/>
<logger name="com.scb.comgm" level="debug"/>
<!-- 根日志设置 -->
<Root level="info">
<AppenderRef ref="consoleAppender" level="info"/>
<AppenderRef ref="debugFileAppender" level="debug"/>
<AppenderRef ref="infoFileAppender" level="info"/>
<AppenderRef ref="warnFileAppender" level="warn"/>
<AppenderRef ref="errorFileAppender" level="error"/>
</Root>
</Loggers>
</configuration>
自定义插件内容如下:
package com.scb.comgm.config.plugs;
import cn.hutool.core.map.MapUtil;
import com.scb.comgm.config.SensitiveDataRule;
import com.scb.comgm.util.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.apache.logging.log4j.util.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* log4j2 脱敏插件
* 继承AbstractStringLayout
**/
@Plugin(name = "CustomPatternLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
public class CustomPatternLayout extends AbstractStringLayout {
public final static Logger logger = LoggerFactory.getLogger(CustomPatternLayout.class);
/**
* 要匹配的正则表达式map
*/
private static final Map<String, Pattern> REG_PATTERN_MAP = new HashMap<>();
private static final Map<String, String> KEY_REG_MAP = new HashMap<>();
private final PatternLayout patternLayout;
protected CustomPatternLayout(Charset charset, String pattern, Boolean disableAnsi, Boolean noConsoleNoAnsi) {
super(charset);
patternLayout = PatternLayout.newBuilder().withDisableAnsi(disableAnsi)
.withNoConsoleNoAnsi(noConsoleNoAnsi).withPattern(pattern).build();
}
/**
* 创建插件
*/
@PluginFactory
public static Layout createLayout(@PluginAttribute(value = "pattern") final String pattern,
@PluginAttribute(value = "charset") final Charset charset,
@PluginAttribute(value = "disableAnsi") final Boolean disableAnsi,
@PluginAttribute(value = "noConsoleNoAnsi") final Boolean noConsoleNoAnsi) {
return new CustomPatternLayout(charset, pattern, disableAnsi, noConsoleNoAnsi);
}
private void initRule() {
try {
Map<String, String> regularMap = SensitiveDataRule.regularMap;
if (MapUtil.isEmpty(regularMap)) {
return;
}
regularMap.forEach((a, b) -> {
if (Strings.isNotBlank(a)) {
Map<String, String> collect = Arrays.stream(a.split(",")).collect(Collectors.toMap(c -> c, w -> b, (key1, key2) -> key1));
KEY_REG_MAP.putAll(collect);
}
Pattern compile = Pattern.compile(b);
REG_PATTERN_MAP.put(b, compile);
});
} catch (Exception e) {
logger.info(">>>>>> 初始化日志脱敏规则失败 ERROR:{0}", e);
}
}
/**
* 处理日志信息,进行脱敏
* 1.判断配置文件中是否已经配置需要脱敏字段
* 2.判断内容是否有需要脱敏的敏感信息
* 2.1 没有需要脱敏信息直接返回
* 2.2 处理: 身份证 ,姓名,手机号,地址敏感信息
*/
public String hideMarkLog(String logStr) {
try {
//若初始化日志失败,则重新初始化一次
if (MapUtil.isEmpty(KEY_REG_MAP) || MapUtil.isEmpty(REG_PATTERN_MAP)) {
initRule();
}
//1.判断配置文件中是否已经配置需要脱敏字段
if (Strings.isBlank(logStr) || MapUtil.isEmpty(KEY_REG_MAP) || MapUtil.isEmpty(REG_PATTERN_MAP)) {
return logStr;
}
//2.判断内容是否有需要脱敏的敏感信息
Set<String> charKeys = KEY_REG_MAP.keySet();
for (String key : charKeys) {
if (logStr.contains(key)) {
String regExp = KEY_REG_MAP.get(key);
logStr = matchingAndEncrypt(logStr, regExp, key);
}
}
return logStr;
} catch (Exception e) {
logger.info(">>>>>>>>> 脱敏处理异常 ERROR:{0}", e);
//如果抛出异常为了不影响流程,直接返回原信息
return logStr;
}
}
/**
* 正则匹配对应的对象。
*
* @param msg 日志对象
* @param regExp 正则匹配
* @param key 字段
* @return 找到对应对象
*/
private String matchingAndEncrypt(String msg, String regExp, String key) {
Pattern pattern = Pattern.compile(regExp);
Matcher matcher = pattern.matcher(msg);
Integer hideType = SensitiveDataRule.hideStrTypeMap.entrySet().stream().filter(entry ->
entry.getKey().contains(key)).findFirst().get().getValue();
int length = key.length() + 5;
// boolean names = Log4j2Rule.USER_NAME_STR.contains(key);
String hiddenStr = "***";
StringBuffer result = new StringBuffer(msg);
int i = 0;
while (matcher.find()) {
String originStr = matcher.group();
// 计算关键词和需要脱敏词的距离小于5。
i = msg.indexOf(originStr, i);
if (i < 0) {
continue;
}
int span = i - length;
int startIndex = Math.max(span, 0);
String substring = msg.substring(startIndex, i);
if (Strings.isBlank(substring) || !substring.contains(key)) {
i += key.length();
continue;
}
//日志脱敏只需要对密码和手机号
hiddenStr = StringUtils.hideMarkStr(originStr, hideType);
msg = result.replace(i, i + originStr.length(), hiddenStr).toString();
}
return msg;
}
@Override
public String toSerializable(LogEvent event) {
return hideMarkLog(patternLayout.toSerializable(event));
}
}
现在控制台日志如下:
