国家日益对用户敏感信息关注增加。现在如果日志中存在用户的敏感信息,如生日,身份证号、手机号、家庭地址。都需要在日志中进行混淆,不能明文输出。以防敏感信息泄露。
为了进行统一的解决方案,就想对指定的field进行混淆。
查看了一下生产环境上的log日志。
输出object的时候大概分为三种格式。
accountNumber=123456
avro 生成的bean如 "correlationKey": "13548d68-0f8d-40fe-899c-b96481ea0fad"
最后方案选型就选择通过正则表达式找到所有对象。
正则表达式如下:(\\\"|')?keyword(\\\"|')?(=|:)\\s?(\\\"|')?\\w+(\\\"|')?
import ch.qos.logback.classic.pattern.MessageConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import net.homecredit.mdt.web.core.utils.RegexMixLogUtils;
import org.apache.commons.lang3.ArrayUtils;
public class SensitiveInformationLoggingConverter extends MessageConverter {
RegexMixLogUtils regexMixLogUtils = RegexMixLogUtils.getSoloInstance();
@Override
public String convert(ILoggingEvent event) {
if (needToMix(event)) {
String log = super.convert(event);
return regexMixLogUtils.mixLog(log);
} else {
return super.convert(event);
}
}
private boolean needToMix(ILoggingEvent event) {
if (ArrayUtils.isEmpty(event.getArgumentArray())) {
return false;
}
if (hasThrowable(event)) {
return false;
}
/**
* if there is any other approaches to skip mix log add logic here.
*/
return true;
}
private boolean hasThrowable(ILoggingEvent event) {
Object[] objects = event.getArgumentArray();
if (objects != null) {
for (Object o : objects) {
if (Throwable.class.isAssignableFrom(o.getClass())) {
return true;
}
}
}
return false;
}
}
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
public class RegexMixLogUtils {
static private final RegexMixLogUtils soloInstance = new RegexMixLogUtils();
public static RegexMixLogUtils getSoloInstance() {
return soloInstance;
}
private Map patterns = new LinkedHashMap<>();
private String patternFormat = "(\\\"|')?%s(\\\"|')?(=|:)\\s?(\\\"|')?(?<%s>\\w+)(\\\"|')?";
private String customizedPatternFormat = "(\\\"|')?%s(\\\"|')?(=|:)\\s?(\\\"|')?(?<%s>%s)(\\\"|')?";
private RegexMixLogUtils() {
Properties properties = new Properties();
String keysStr = null;
String keyPatternStr = null;
try {
properties.load(RegexMixLogUtils.class.getClassLoader().getResourceAsStream("application.properties"));
keysStr = properties.getProperty("application.logging.mix.keys");
keyPatternStr = properties.getProperty("application.logging.mix.key-patterns");
} catch (IOException e) {
log.warn("can't get the config file.", e);
}
if (!StringUtils.isEmpty(keysStr)) {
String[] keys = keysStr.split(",");
for (String key : keys) {
patterns.put(key, Pattern.compile(String.format(patternFormat, key, key)));
}
}
if (!StringUtils.isEmpty(keyPatternStr)) {
String[] keys = keyPatternStr.split(",");
for (String key : keys) {
String[] keyPattern = key.split(":");
if (keyPattern.length == 2) {
patterns.put(keyPattern[0], Pattern.compile(String.format(customizedPatternFormat, keyPattern[0], keyPattern[0], keyPattern[1])));
}
}
}
}
public String mixLog(String log) {
if (patterns.size() == 0) {
return log;
}
Set keySet = patterns.keySet();
for (String key : keySet) {
Pattern pattern = patterns.get(key);
Matcher matcher = pattern.matcher(log);
while (matcher.find()) {
String matched = matcher.group(key);
if (StringUtils.isBlank(matched) || StringUtils.equalsIgnoreCase("null", matched)) {
continue;
}
String group = matcher.group();
String encoded = encode(matched);
String groupEncode = group.replace(matched, encoded);
log = log.replace(group, groupEncode);
}
}
return log;
}
private String encode(String key) {
if (StringUtils.isBlank(key)) {
return key;
}
return DigestUtils.md5Hex(key);
}
}
需要添加的配置文件如下。
application.logging.mix.keys=phoneNumber,personalId,password,accountNumber,idCardNumber,accountName
application.logging.mix.key-patterns=dateOfBirth:\\d{4}-\\d{2}-\\d{2}
第一类是字段名。
第二类是特殊的正则表达方式。如生日。yyyy-MM-dd
%d{yyyy-MM-dd HH:mm:ss.SSS,Asia/Shanghai} [%X{correlationId}] [%level] [%thread] [%c:%L] - %mixedMessage
utf8
Logback配置。
最开始还尝试了其他方案。但是都以种种理由推翻了。
方案1:通过java反射遍历对象。找到keyword的field。混淆存到map中。打印日志后,把原始值回复。
缺点:真实改变了所有的参数,以及返回值。确实怕有测试没有测到的地方,系统出现莫名的问题。
方案2:通过AOP 在,在log.info 上面加切面。
缺点:无法实现,log不是在spring工厂里面维护的。没有办法通过spring 的aop进行修改。
方案3:toString前 进行AOP
缺点:同上
方案4:最后的正则方案
缺点:中间出现大量的字符串比较,替换。会导致系统大量GC。也没有办法进行简单的过滤。可能对系统性能造成影响。
方案5:通过annotation 去掉敏感信息字段。如Lombok的ToStringIgnore。 JsonIgore
缺点:需要大量的人工。但是不会对系统造成其他的负担。