Log42 的日志脱敏
Log42 的日志脱敏
最近遇到公司的敏感数据脱敏,就想着也不能挨个的日志去找他的,太多了。后来想到logger.info() 这个方法,就在网上找了找对应的信息,可以。有这样实现的,但是实现的功能有的有点太简单,不能变通,有的写得不清楚,我觉得我自己也记个记录吧,也和大家分享下,后期性能优化还没想到。如果有更好的思路,请留言。
代码部分
原来的log4j2 的XML默认的是这样的 。发现在这个PatternLayout中有个定义格式的类。。。
1 <?xml version="1.0" encoding="UTF-8"?>
2 <!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
3 <!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
4 <!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
5 <configuration status="WARN" monitorInterval="30">
6 <!--先定义所有的appender-->
7 <appenders>
8 <!--这个输出控制台的配置-->
9 <console name="Console" target="SYSTEM_OUT">
10 <!--输出日志的格式-->
11 <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
12 </console>
13 <!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,这个也挺有用的,适合临时测试用-->
14 <File name="log" fileName="log/test.log" append="false">
15 <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
16 </File>
17 <!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
18 <RollingFile name="RollingFileInfo" fileName="${sys:user.home}/logs/info.log"
19 filePattern="${sys:user.home}/logs/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log">
20 <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
21 <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
22 <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
23 <Policies>
24 <TimeBasedTriggeringPolicy/>
25 <SizeBasedTriggeringPolicy size="100 MB"/>
26 </Policies>
27 </RollingFile>
28 <RollingFile name="RollingFileWarn" fileName="${sys:user.home}/logs/warn.log"
29 filePattern="${sys:user.home}/logs/$${date:yyyy-MM}/warn-%d{yyyy-MM-dd}-%i.log">
30 <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
31 <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
32 <Policies>
33 <TimeBasedTriggeringPolicy/>
34 <SizeBasedTriggeringPolicy size="100 MB"/>
35 </Policies>
36 <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件,这里设置了20 -->
37 <DefaultRolloverStrategy max="20"/>
38 </RollingFile>
39 <RollingFile name="RollingFileError" fileName="${sys:user.home}/logs/error.log"
40 filePattern="${sys:user.home}/logs/$${date:yyyy-MM}/error-%d{yyyy-MM-dd}-%i.log">
41 <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
42 <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
43 <Policies>
44 <TimeBasedTriggeringPolicy/>
45 <SizeBasedTriggeringPolicy size="100 MB"/>
46 </Policies>
47 </RollingFile>
48 </appenders>
49 <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
50 <loggers>
51 <!--过滤掉spring和mybatis的一些无用的DEBUG信息-->
52 <logger name="org.springframework" level="INFO"></logger>
53 <logger name="org.mybatis" level="INFO"></logger>
54 <root level="all">
55 <appender-ref ref="Console"/>
56 <appender-ref ref="RollingFileInfo"/>
57 <appender-ref ref="RollingFileWarn"/>
58 <appender-ref ref="RollingFileError"/>
59 </root>
60 </loggers>
61 </configuration>
好的,开始写代码。
在其中我们找到了这个类 :PatternLayout 输出日志的格式 ,他便是在输出前进行格式化的类,所以我们继承他是不是就可以自己去定义应该过滤的敏感信息了啊?然后,我继承了这个类:
package com.davinqi.logger;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.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;
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 脱敏插件
*
* @Author qix*****
* @Date 2020-08-20 15:57
* @Version 1.0
**/
@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);
private PatternLayout patternLayout;
protected CustomPatternLayout(Charset charset, String pattern) {
super(charset);
patternLayout = PatternLayout.newBuilder().withPattern(pattern).build();
initRule();
}
/**
* 要匹配的正则表达式map
*/
private static Map<String, Pattern> REG_PATTERN_MAP = new HashMap<>();
private static Map<String, String> KEY_REG_MAP = new HashMap<>();
private void initRule() {
try {
if (MapUtils.isEmpty(MyLog4jRule.regularMap)) {
return;
}
MyLog4jRule.regularMap.forEach((a, b) -> {
if (StringUtils.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:{}", e);
}
}
/**
* 1.判断配置文件中是否已经配置需要脱敏字段
* 2.判断内容是否有需要脱敏的敏感信息
* 2.1 没有需要脱敏信息直接返回
* 2.2 处理: 身份证 ,姓名,手机号敏感信息
*
* @param @param msg
* @param @return
* @return String
*/
public String hideMarkLog(String logStr) {
try {
//1.判断配置文件中是否已经配置需要脱敏字段
if (StringUtils.isBlank(logStr) || MapUtils.isEmpty(KEY_REG_MAP) || MapUtils.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:{}", e);
//如果跑出异常为了不影响流程,直接返回原信息
return logStr;
}
}
/**
* 正则匹配对应的对象。
*
* @param msg
* @param regExp
* @return
*/
private static String matchingAndEncrypt(String msg, String regExp, String key) {
Pattern pattern = REG_PATTERN_MAP.get(regExp);
if (pattern == null) {
logger.info(">>> logger 没有匹配到对应的正则表达式 ");
return msg;
}
Matcher matcher = pattern.matcher(msg);
int length = key.length() + 5;
boolean contains = MyLog4jRule.USER_NAME_STR.contains(key);
String hiddenStr = "";
while (matcher.find()) {
String originStr = matcher.group();
if (contains) {
// 计算关键词和需要脱敏词的距离小于5。
int i = msg.indexOf(originStr);
if (i < 0) {
continue;
}
int span = i - length;
int startIndex = span >= 0 ? span : 0;
String substring = msg.substring(startIndex, i);
if (StringUtils.isBlank(substring) || !substring.contains(key)) {
continue;
}
// // 正则 但是影响性能
// String userNearKey = "(([\\s\\S]*)(" + key + ")(\\S){0,6}(" + originStr + ")([\\s\\S]*))";
// Pattern p = Pattern.compile(userNearKey);
// Matcher m = p.matcher(msg);
// if (!m.matches()) {
// continue;
// }
hiddenStr = hideMarkStr(originStr);
msg = msg.replace(originStr, hiddenStr);
} else {
hiddenStr = hideMarkStr(originStr);
msg = msg.replace(originStr, hiddenStr);
}
}
return msg;
}
/**
* 标记敏感文字规则
*
* @param needHideMark
* @return
*/
private static String hideMarkStr(String needHideMark) {
if (StringUtils.isBlank(needHideMark)) {
return "";
}
int startSize = 0, endSize = 0, mark = 0, length = needHideMark.length();
StringBuffer hideRegBuffer = new StringBuffer("(\\S{");
StringBuffer replaceSb = new StringBuffer("$1");
if (length > 4) {
int i = length / 3;
startSize = i;
endSize = i;
} else {
startSize = 1;
endSize = 0;
}
mark = length - startSize - endSize;
for (int i = 0; i < mark; i++) {
replaceSb.append("*");
}
hideRegBuffer.append(startSize).append("})\\S*(\\S{").append(endSize).append("})");
replaceSb.append("$2");
needHideMark = needHideMark.replaceAll(hideRegBuffer.toString(), replaceSb.toString());
return needHideMark;
}
/**
* LOG4J2-783 use platform default by default,
* so do not specify defaultString for charset
*
* @param pattern
* @param charset
* @return
*/
@PluginFactory
public static Layout createLayout(@PluginAttribute(value = "pattern") final String pattern,
@PluginAttribute(value = "charset") final Charset charset) {
return new CustomPatternLayout(charset, pattern);
}
@Override
public String toSerializable(LogEvent event) {
return hideMarkLog(patternLayout.toSerializable(event));
}
}
一定要把 正则规则Pattern初始化了,要不然你得日志非常的耗时,业务流程也很慢。-||
package com.davinqi.logger;
import java.util.HashMap;
import java.util.Map;
/**
* @Author qix*********
* @Date 2020-08-20 16:11
* @Version 1.0
* <p>
* 现在拦截加密的日志有三类:
* 1,身份证
* 2,姓名
* 3,身份证号
**/
public class MyLog4jRule {
/**
* 正则匹配 关键词 类别
*/
public static Map<String, String> regularMap = new HashMap<>();
/**
* TODO 可配置
* 此项可以后期放在配置项中
*/
public static final String USER_NAME_STR = "Name,name,联系人,姓名";
public static final String USER_IDCARD_STR = "empCard,idCard,身份证,证件号";
public static final String USER_PHONE_STR = "mobile,Phone,phone,电话,手机";
/**
* "(\\d{6}\\d{4}(([0][1-9])|([1][0-2]))(([0][1-9])|([1][0-9])|([2][0-9])|([3][0-1]))[0-9]{3}[xX0-9]) | (\\d{6}\\d{2}(([0][1-9])|([1][0-2]))(([0][1-9])|([1][0-9])|([2][0-9])|([3][0-1]))[0-9]{3})";
*/
private static String IDCARD_REGEXP = "(\\d{17}[0-9Xx]|\\d{14}[0-9Xx])";
private static String USERNAME_REGEXP = "[\\u4e00-\\u9fa5]{2,4}";
private static String PHONE_REGEXP = "(?<!\\d)(?:(?:1[3456789]\\d{9})|(?:861[356789]\\d{9}))(?!\\d)";
static {
regularMap.put(USER_NAME_STR, USERNAME_REGEXP);
regularMap.put(USER_IDCARD_STR, IDCARD_REGEXP);
regularMap.put(USER_PHONE_STR, PHONE_REGEXP);
}
}
此类里放置了定义的敏感key和正则表达式匹配,我自己定义的规则是:脱敏的日志中包含这个这些关键词,然后正则匹配这些,脱敏的词汇,进行自己定义的格式脱敏。因为汉字太多了。我限制了 在关键词后5个以内有汉字能匹配的,才去脱敏。
最后吧想脱敏级别的日志替换成自己定义的PatternLayout
PatternLayout -> CustomPatternLayout
这块的关键词匹配会随着数量变大时间复杂度,也会变大看看大家有什么优化的点子呢?
by davinqi