日志脱敏首先要搞清楚,影响的数据范围,是要全局支持日志脱敏,还是只针对部分代码。
如果涉及到敏感数据的业务代码较少,建议写个数据脱敏工具类,在打印日志的时候调用,灵活可靠,影响范围小。
一、第一种方案:全局方式
针对log4j2的日志脱敏实现方案,重写rewrite方法,对敏感数据进行脱敏操作
需要在log4j2.xml 引入此插件,插件名称为DataMaskingRewritePolicy。
1、重写rewrite方法
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Objects;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.appender.rewrite.RewritePolicy;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.impl.Log4jLogEvent;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.logging.log4j.message.SimpleMessage;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
/**
*
* 针对log4j2的日志脱敏实现方案,重写rewrite方法,调整日志内容
*
* 需要在log4j2.xml 引入此插件,插件名称为DataMaskingRewritePolicy
*
*/
@Plugin(name = "DataMaskingRewritePolicy", category = "Core", elementType = "rewritePolicy", printObject = true)
public class DataMaskingRewritePolicy implements RewritePolicy {
/*
* 脱敏符号
*/
private static final String ASTERISK = "****";
/*
* 引号
*/
private static final String QUOTATION_MARK = "\"";
// 使用静态内部类创建对象,节省空间
private static class StaticDataMaskingRewritePolicy {
private static final DataMaskingRewritePolicy dataMaskingRewritePolicy = new DataMaskingRewritePolicy();
}
// 需要加密的字段配置数组
private static final String[] encryptionKeyArrays = { "password", "expireYear", "expireMonth",
"cvv" };
// 将数组转换为集合,方便查找
private static final List<String> encryptionKeys = new ArrayList<>();
public DataMaskingRewritePolicy() {
if (CollectionUtils.isEmpty(encryptionKeys)) {
encryptionKeys.addAll(Arrays.asList(encryptionKeyArrays));
}
}
/**
* 日志修改方法,可以对日志进行过滤,修改
*
* @param logEvent
* @return
*/
@Override
public LogEvent rewrite(LogEvent logEvent) {
if (!(logEvent instanceof Log4jLogEvent)) {
return logEvent;
}
Log4jLogEvent log4jLogEvent = (Log4jLogEvent) logEvent;
Message message = log4jLogEvent.getMessage();
if ((message instanceof ParameterizedMessage)) {
ParameterizedMessage parameterizedMessage = (ParameterizedMessage) message;
Object[] params = parameterizedMessage.getParameters();
if (params == null || params.length <= 0) {
return logEvent;
}
Object[] newParams = new Object[params.length];
for (int i = 0; i < params.length; i++) {
try {
if(params[i] instanceof JSONObject){
JSONObject jsonObject = (JSONObject) params[i];
// 处理json格式的日志
newParams[i] = encryptionJson(jsonObject, encryptionKeys);
} else{
newParams[i] = params[i];
}
} catch (Exception e) {
newParams[i] = params[i];
}
}
ParameterizedMessage m = new ParameterizedMessage(parameterizedMessage.getFormat(), newParams,
parameterizedMessage.getThrowable());
Log4jLogEvent.Builder builder = log4jLogEvent.asBuilder().setMessage(m);
return builder.build();
} else if (message instanceof SimpleMessage) {
SimpleMessage newMessage = decryptionSimpleMessage((SimpleMessage) message);
Log4jLogEvent.Builder builder = log4jLogEvent.asBuilder().setMessage(newMessage);
return builder.build();
} else {
return logEvent;
}
}
/**
* 单例模式创建(静态内部类模式)
*
* @return
*/
@PluginFactory
public static DataMaskingRewritePolicy createPolicy() {
return StaticDataMaskingRewritePolicy.dataMaskingRewritePolicy;
}
private SimpleMessage decryptionSimpleMessage(SimpleMessage simpleMessage) {
String messsage = simpleMessage.toString();
String newMessage = messsage;
if (!StringUtils.isEmpty(messsage)) {
boolean isContain = encryptionKeys.stream().anyMatch(key -> StringUtils.contains(messsage, key));
// 包含敏感词
if (isContain) {
for (String key : encryptionKeyArrays) {
int keyLength = key.length();
// 敏感词
String targetStr = new String("<" + key + ">");
StringBuffer targetSb = new StringBuffer(targetStr);
int startIndex = newMessage.indexOf(targetStr);
/*
* 如<password>123456</password>替换为 <password>****</password>
*/
if (startIndex > -1 && newMessage.indexOf(targetSb.append(ASTERISK).append("</").append(key).append(">").toString()) == -1) {
int endIndex = newMessage.indexOf(targetStr);
if (endIndex > -1) {
newMessage = newMessage.substring(0, startIndex + keyLength + 2) + ASTERISK
+ newMessage.substring(endIndex, newMessage.length());
}
}
/*
* 如password:123456替换为password:****
*/
targetStr = key + ":";
if (newMessage.indexOf(targetStr) > -1 && newMessage.indexOf(targetStr + ASTERISK) == -1) {
startIndex = newMessage.indexOf(targetStr) + keyLength + 1;
String endMessage = newMessage.substring(startIndex, newMessage.length());
int endIndex = endMessage.indexOf(",");
if (endIndex > -1) {
newMessage = newMessage.substring(0, startIndex) + ASTERISK
+ endMessage.substring(endIndex, endMessage.length());
} else if (endMessage.indexOf(",") == -1 && endMessage.indexOf("=") == -1) {
newMessage = newMessage.substring(0, startIndex) + ASTERISK;
}
}
/*
* 如password=123456替换为password=****
*/
if (newMessage.indexOf(key + "=") > -1 && newMessage.indexOf(key + "=" + ASTERISK) == -1) {
startIndex = newMessage.indexOf(key + "=") + keyLength + 1;
String endMessage = newMessage.substring(startIndex, newMessage.length());
int endIndex = endMessage.indexOf(",");
if (endIndex > -1) {
newMessage = newMessage.substring(0, startIndex) + ASTERISK
+ endMessage.substring(endIndex, endMessage.length());
} else if (endMessage.indexOf(",") == -1 && endMessage.indexOf("=") == -1) {
newMessage = newMessage.substring(0, startIndex) + ASTERISK;
}
}
/*
* 如"password":"123456" 替换为"password":"****"
*/
String qmKey = QUOTATION_MARK + key + QUOTATION_MARK + ":";
if (newMessage.indexOf(qmKey) > -1 && newMessage.indexOf(qmKey + ASTERISK) == -1) {
startIndex = newMessage.indexOf(qmKey) + keyLength + 3;
String endMessage = newMessage.substring(startIndex, newMessage.length());
int endIndex = endMessage.indexOf(",");
if (endIndex > -1) {
newMessage = newMessage.substring(0, startIndex) + QUOTATION_MARK + ASTERISK
+ QUOTATION_MARK + endMessage.substring(endIndex, endMessage.length());
} else if (endMessage.indexOf(",") == -1 && endMessage.indexOf("=") == -1) {
newMessage = newMessage.substring(0, startIndex) + QUOTATION_MARK + ASTERISK
+ QUOTATION_MARK;
}
}
}
return new SimpleMessage(newMessage);
}
}
return simpleMessage;
}
/**
* 处理日志,递归获取值
*
*/
private Object encryptionJson(Object object, List<String> encryptionKeys) {
String jsonString = JSON.toJSONString(object);
if (object instanceof JSONObject) {
JSONObject json = JSON.parseObject(jsonString);
boolean isContain = encryptionKeys.stream().anyMatch(key -> StringUtils.contains(jsonString, key));
if (isContain) {
// 判断当前字符串中有没有key值
Set<String> keys = json.keySet();
keys.forEach(key -> {
boolean result = encryptionKeys.stream().anyMatch(ekey -> Objects.equal(ekey, key));
if (result) {
String value = json.getString(key);
// 加密
json.put(key, ASTERISK);
} else {
json.put(key, encryptionJson(json.get(key), encryptionKeys));
}
});
}
return json;
} else if (object instanceof JSONArray) {
JSONArray jsonArray = JSON.parseArray(jsonString);
for (int i = 0; i < jsonArray.size(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
// 转换
jsonArray.set(i, encryptionJson(jsonObject, encryptionKeys));
}
return jsonArray;
}
return object;
}
}
2、修改log4j2.xml配置,增加Rewrite重新日志配置
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<properties>
<!--设置日志文件存储路径为tomcat/logs/${APP_NAME}-->
<Property name="LOG_FILE_PATH">${sys:catalina.home}/logs/${APP_NAME}</Property>
<!--设置日志输出格式-->
<Property name="PATTERN_FORMAT">[%t][%d{yyyyMMdd HH:mm:ss}][%-5p][%c{3}:%L] - %m%n</Property>
<Property name="LOG_LEVEL">info</Property>
</properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="[Shop][%t][%d{yyyyMMdd HH:mm:ss}][%-5p][%c{3}:%L] - %m%n"/>
</Console>
<!-- 配置重写日志 -->
<Rewrite name="rewrite">
<DataMaskingRewritePolicy/>
<AppenderRef ref="Console"/>
<!-- 将catalina日志重写 -->
<!-- <AppenderRef ref="Catalina"/> -->
</Rewrite>
</Appenders>
<Loggers>
<logger name="org.springframework" level="${LOG_LEVEL}">
<AppenderRef ref="Console" />
</logger>
<logger name="org.test" level="${LOG_LEVEL}">
<AppenderRef ref="rewrite" />
</logger>
<Root level="info">
<!-- <AppenderRef ref="Console" /> -->
<!-- <AppenderRef ref="RollingInfoFile" /> -->
</Root>
</Loggers>
</configuration>
3、单元测试
@org.junit.Test
public void testLog4j2JSONString(){
Runtime r = Runtime.getRuntime();
r.gc();//计算内存前先垃圾回收一次
long start = System.currentTimeMillis();//开始Time
long startMem = r.totalMemory(); // 开始Memory
for (int i = 0; i < 1; i++) {
logger.info("\"password\":\"123456\",\"expireYear\":\"2025\",\"expireMonth\":\"10\",\"cvv\":\"844\"");
}
//输出
long endMem =r.freeMemory(); // 末尾Memory
long end = System.currentTimeMillis();//末尾Time
System.out.println("用时消耗: "+String.valueOf(end - start)+"ms");
System.out.println("内存消耗: "+String.valueOf((startMem- endMem)/1024)+"KB");
}
二、第二种方案:自定义脱敏工具类
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
/**
* String数据脱敏处理
*
*/
public class StringMaskUtil {
/*
* 脱敏符号
*/
private static final String ASTERISK = "****";
// 需要脱敏的字段配置数组
private static final String[] encryptionKeyArrays = { "password", "expireYear", "expireMonth","cvv","number"};
/*
* 引号
*/
private static final String QUOTATION_MARK = "\"";
// 将数组转换为集合,方便查找
private static final List<String> encryptionKeys = new ArrayList<>();
// 使用静态内部类创建对象,节省空间
private static class StaticStringMaskUtil {
private static final StringMaskUtil stringMaskUtil = new StringMaskUtil();
}
public StringMaskUtil() {
if (CollectionUtils.isEmpty(encryptionKeys)) {
encryptionKeys.addAll(Arrays.asList(encryptionKeyArrays));
}
}
public static StringMaskUtil instance(){
return StaticStringMaskUtil.stringMaskUtil;
}
/**
* 支持的格式
*
* 1、<key>value</key>
* 2、key=value,key1=value1
* 3、key:value,key1:value1
* 4、"key":"value","key1":"value1"
*
* @param messsage
* @return
*/
public String mask(String messsage){
String newMessage = messsage;
if (!StringUtils.isEmpty(messsage)) {
boolean isContain = encryptionKeys.stream().anyMatch(key -> StringUtils.contains(messsage, key));
// 包含敏感词
if (isContain) {
for (String key : encryptionKeys) {
int keyLength = key.length();
// 敏感词
String targetStr = new String("<" + key + ">");
StringBuffer targetSb = new StringBuffer(targetStr);
int startIndex = newMessage.indexOf(targetStr);
/*
* 如<password>123456</password>替换为 <password>****</password>
*/
if (startIndex > -1 && newMessage.indexOf(targetSb.append(ASTERISK).append("</").append(key).append(">").toString()) == -1) {
int endIndex = newMessage.indexOf("</" + key + ">");
if (endIndex > -1) {
newMessage = newMessage.substring(0, startIndex + keyLength + 2) + ASTERISK
+ newMessage.substring(endIndex, newMessage.length());
}
}
/*
* 如,password:123456替换为password:****
*/
targetStr =","+ key + ":";
if (newMessage.indexOf(targetStr) > -1 && newMessage.indexOf(targetStr + ASTERISK) == -1) {
startIndex = newMessage.indexOf(targetStr) + keyLength + 2;
String endMessage = newMessage.substring(startIndex, newMessage.length());
int endIndex = endMessage.indexOf(",");
if (endIndex > -1) {
newMessage = newMessage.substring(0, startIndex) + ASTERISK
+ endMessage.substring(endIndex, endMessage.length());
} else if (endMessage.indexOf(",") == -1 && endMessage.indexOf("=") == -1) {
newMessage = newMessage.substring(0, startIndex) + ASTERISK;
}
}else if(newMessage.startsWith(key + ":")){
startIndex = keyLength + 1;
String endMessage = newMessage.substring(startIndex, newMessage.length());
int endIndex = endMessage.indexOf(",");
if (endIndex > -1) {
newMessage = newMessage.substring(0, startIndex) + ASTERISK
+ endMessage.substring(endIndex, endMessage.length());
} else if (endMessage.indexOf(",") == -1 && endMessage.indexOf("=") == -1) {
newMessage = newMessage.substring(0, startIndex) + ASTERISK;
}
}
/*
* 如,password=123456替换为password=****
*/
targetStr =","+ key + "=";
if (newMessage.indexOf(targetStr) > -1 && newMessage.indexOf(key + "=" + ASTERISK) == -1) {
startIndex = newMessage.indexOf(targetStr) + keyLength + 2;
String endMessage = newMessage.substring(startIndex, newMessage.length());
int endIndex = endMessage.indexOf(",");
if (endIndex > -1) {
newMessage = newMessage.substring(0, startIndex) + ASTERISK
+ endMessage.substring(endIndex, endMessage.length());
} else if (endMessage.indexOf(",") == -1 && endMessage.indexOf("=") == -1) {
newMessage = newMessage.substring(0, startIndex) + ASTERISK;
}
}else if(newMessage.startsWith(key + "=")){
startIndex = keyLength + 1;
String endMessage = newMessage.substring(startIndex, newMessage.length());
int endIndex = endMessage.indexOf(",");
if (endIndex > -1) {
newMessage = newMessage.substring(0, startIndex) + ASTERISK
+ endMessage.substring(endIndex, endMessage.length());
} else if (endMessage.indexOf(",") == -1 && endMessage.indexOf("=") == -1) {
newMessage = newMessage.substring(0, startIndex) + ASTERISK;
}
}
/*
* 如"password":"123456" 替换为"password":"****"
*/
String qmKey = QUOTATION_MARK + key + QUOTATION_MARK + ":";
if (newMessage.indexOf(qmKey) > -1 && newMessage.indexOf(qmKey + ASTERISK) == -1) {
startIndex = newMessage.indexOf(qmKey) + keyLength + 3;
String endMessage = newMessage.substring(startIndex, newMessage.length());
int endIndex = endMessage.indexOf(",");
if (endIndex > -1) {
newMessage = newMessage.substring(0, startIndex) + QUOTATION_MARK + ASTERISK
+ QUOTATION_MARK + endMessage.substring(endIndex, endMessage.length());
} else if (endMessage.indexOf(",") == -1 && endMessage.indexOf("=") == -1) {
newMessage = newMessage.substring(0, startIndex) + QUOTATION_MARK + ASTERISK
+ QUOTATION_MARK;
}
}
}
}
}
return newMessage;
}
}
自定义脱敏工具类单元测试
@org.junit.Test
public void testLog4jString(){
Runtime r = Runtime.getRuntime();
r.gc();//计算内存前先垃圾回收一次
long start = System.currentTimeMillis();//开始Time
long startMem = r.totalMemory(); // 开始Memory
for (int i = 0; i < 1; i++) {
System.out.println(StringMaskUtil.instance().mask("oldpassword=123456,expireYear=2022,expireMonth=12,cvv=844"));
System.out.println(StringMaskUtil.instance().mask("password:123456,aaexpireYear:2022,expireMonth:12,cvv:844"));
System.out.println(StringMaskUtil.instance().mask("password=123456,expireYear=2022,expireMonth=12,cvv=844"));
System.out.println(StringMaskUtil.instance().mask("\"oldpassword\":\"123456\",\"expireYear\":\"2025\",\"expireMonth\":\"10\",\"cvv\":\"844\""));
System.out.println(StringMaskUtil.instance().mask("<password>123456</password><expireYear>123456</expireYear><expireMonth>123456</expireMonth><cvv>123456</cvv>"));
}
//输出
long endMem =r.freeMemory(); // 末尾Memory
long end = System.currentTimeMillis();//末尾Time
System.out.println("用时消耗: "+String.valueOf(end - start)+"ms");
System.out.println("内存消耗: "+String.valueOf((startMem- endMem)/1024)+"KB");
}