背景:公司日志数据不能暴露账号密码登个人关键信息
常见脱密方式:(推荐使用第二种方式)
1.xml配置对应的key然后对其value进行相应规则的脱敏(此种方式对于没有key的值无法脱敏并且系统重也不好穷尽所有需要脱敏的key)
2.采用正则表达式匹配对应的关键信息进行脱敏(满足正则表达是即可正确的脱敏相应的内容)
下面就对第二种方式详细说明:
1.定义一个SensitiveLogDataConverter
import ch.qos.logback.classic.pattern.MessageConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import cn.hutool.core.util.StrUtil;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class SensitiveLogDataConverter extends MessageConverter {
// 脱敏符号 *
public static final String SYMBOL_STAR = "*";
/**
* 0 加密前三位
* 1 加密中间四位
* 2 加密后面四位
*/
public static Integer logMode = 1;
/**
* 是否开启加密规则 默认为false
*/
public static boolean logSensitive = false;
/**
* 手机号
*/
private static final Pattern PHONE_PATTERN = Pattern.compile("(?<!\\d)(1\\d{10})(?!\\d)");
private static final Pattern LANDLINE_AREA = Pattern.compile("[0][1-9]{2,3}-[0-9]{5,10}"); // 验证带区号的
private static final Pattern LANDLINE = Pattern.compile("[1-9]{1}[0-9]{5,8}"); // 验证没有区号的
/**
* 证件号码
*/
private static final Pattern ID_NUMBER = Pattern.compile("((1[1-5])|(2[1-3])|(3[1-7])|(4[1-6])|(5[0-4])|(6[1-5])|71|81|82|91)[0-9]{4}(18|19|20)[0-9]{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)[0-9]{3}[0-9Xx]");
/**
* 银行卡
*/
//private static final Pattern BANK_CARD_NUMBER = Pattern.compile("^(?:62|88|99|19\\d{2}|4\\d{3}|5[1-5]\\d{2}|6\\d{3}|81\\d{2})\\d{10}|^62\\d{12,17}|^[0-9]{16,19}$");
private static final Pattern BANK_CARD_NUMBER = Pattern.compile("[0-9]{16,19}");
/**
* 邮箱符号
*/
private static final Pattern EMAIL = Pattern.compile("\\w[-\\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\\.)+[A-Za-z]{2,14}");
//private static final Pattern ADDRESS = Pattern.compile("/^([\\u4e00-\\u9fa5]+(?:省|市|自治区)){0,1}([\\u4e00-\\u9fa5]+(?:市|区|县|州|盟)){0,1}([\\u4e00-\\u9fa5]+(?:街道|镇|乡)){0,1}([\\u4e00-\\u9fa5]+(?:号|村|社)){0,1}([\\u4e00-\\u9fa5]+(?:路|街|巷)){0,1}([\\u4e00-\\u9fa5]+(?:弄|号楼|栋|单元)){0,1}([0-9A-Za-z]{5,}){0,1}$/");
//
private static final Pattern ADDRESS = Pattern.compile("([^区]+区)|([^镇]+镇)|([^路]+路)|([^街]+街)|([^单元]+单元)|([^公司]+公司)|([^室]+室)|([^户]+户)");
@Override
public String convert(ILoggingEvent event) {
String logMsg = event.getFormattedMessage();
if (logSensitive) {
try {
final Set<String> email = validateEmail(logMsg);
if (!email.isEmpty()) {
for (String param : email) {
logMsg = logMsg.replaceAll(param, emailEncrypt(param));
}
}
final Set<String> phoneNumber = validatePhoneNumber(logMsg);
//电话号码加密
if (!phoneNumber.isEmpty()) {
for (String param : phoneNumber) {
logMsg = logMsg.replaceAll(param, mobileEncrypt(param));
}
}
final Set<String> landlineArea = validateLandlineArea(logMsg);
if (!phoneNumber.isEmpty()) {
for (String param : landlineArea) {
logMsg = logMsg.replaceAll(param, idNumberEncrypt(param));
}
}
final Set<String> IDNumber = validateIDNumber(logMsg);
if (!IDNumber.isEmpty()) {
for (String param : IDNumber) {
logMsg = logMsg.replaceAll(param, idNumberEncrypt(param));
}
}
final Set<String> cardNumber = validateCardNumber(logMsg);
if (!cardNumber.isEmpty()) {
for (String param : cardNumber) {
logMsg = logMsg.replaceAll(param, idNumberEncrypt(param));
}
}
final Set<String> bankCardNumber = validateBankCardNumber(logMsg);
if (!bankCardNumber.isEmpty()) {
for (String param : bankCardNumber) {
logMsg = logMsg.replaceAll(param, idNumberEncrypt(param));
}
}
logMsg = dealAddress(logMsg);
return logMsg;
} catch (Exception e) {
e.printStackTrace();
return super.convert(event);
}
} else {
return logMsg;
}
}
private static String dealAddress(String logMsg) {
char[] charArray = logMsg.toCharArray();
if (logMsg.contains("市")) {
while (logMsg.contains("市")) {
for (int i = logMsg.indexOf("市"); i < logMsg.length() - 1; i++) {
if (checkAddressEnd(charArray[i])) {
break;
}
charArray[i] = SYMBOL_STAR.charAt(0);
}
logMsg = String.valueOf(charArray);
}
} else {
if (logMsg.contains("省")) {
while (logMsg.contains("省")) {
for (int i = logMsg.indexOf("省"); i < logMsg.length() - 1; i++) {
if (checkAddressEnd(charArray[i])) {
break;
}
charArray[i] = SYMBOL_STAR.charAt(0);
}
logMsg = String.valueOf(charArray);
}
}
}
return String.valueOf(charArray);
}
private static boolean checkAddressEnd(char c) {
return String.valueOf(c).equals(" ") || String.valueOf(c).equals(":") || String.valueOf(c).equals(";") ||
String.valueOf(c).equals(":") || String.valueOf(c).equals(";") || String.valueOf(c).equals(",")
|| String.valueOf(c).equals(",") || String.valueOf(c).equals("“") || String.valueOf(c).equals("\"");
}
/**
* 获取日志字符串内容中符合手机号
*/
private static Set<String> validatePhoneNumber(String param) {
Set<String> set = new HashSet<>();
// 匹配手机号
Matcher phoneMatcher = PHONE_PATTERN.matcher(param);
while (phoneMatcher.find()) {
set.add(phoneMatcher.group());
}
return set;
}
private static Set<String> validateLandline(String param) {
Set<String> set = new HashSet<>();
// 匹配手机号
Matcher phoneMatcher = LANDLINE.matcher(param);
while (phoneMatcher.find()) {
set.add(phoneMatcher.group());
}
return set;
}
private static Set<String> validateLandlineArea(String param) {
Set<String> set = new HashSet<>();
// 匹配手机号
Matcher phoneMatcher = LANDLINE_AREA.matcher(param);
while (phoneMatcher.find()) {
set.add(phoneMatcher.group());
}
return set;
}
private static Set<String> validateIDNumber(String param) {
Set<String> set = new HashSet<>();
Matcher phoneMatcher = ID_NUMBER.matcher(param);
while (phoneMatcher.find()) {
set.add(phoneMatcher.group());
}
return set;
}
private static Set<String> validateCardNumber(String param) {
Set<String> set = new HashSet<>();
Matcher phoneMatcher = CARD_NUMBER.matcher(param);
while (phoneMatcher.find()) {
set.add(phoneMatcher.group());
}
return set;
}
private static Set<String> validateBankCardNumber(String param) {
Set<String> set = new HashSet<>();
Matcher phoneMatcher = BANK_CARD_NUMBER.matcher(param);
while (phoneMatcher.find()) {
set.add(phoneMatcher.group());
}
return set;
}
private static Set<String> validateEmail(String param) {
Set<String> set = new HashSet<>();
Matcher phoneMatcher = EMAIL.matcher(param);
while (phoneMatcher.find()) {
set.add(phoneMatcher.group());
}
return set;
}
private static Set<String> validateAddress(String param) {
Set<String> set = new HashSet<>();
Matcher phoneMatcher = ADDRESS.matcher(param);
while (phoneMatcher.find()) {
set.add(phoneMatcher.group());
}
return set;
}
/**
* 手机号脱敏
*
* @param mobile
* @return
*/
public static String mobileEncrypt(String mobile) {
if (StrUtil.isEmpty(mobile) || (mobile.length() != 11)) {
return mobile;
}
switch (logMode) {
case 0:
return mobile.replaceAll("(\\d{3})(\\d{4})(\\d{4})", "***$2$3");
case 1:
return mobile.replaceAll("(\\d{3})(\\d{4})(\\d{4})", "$1****$3");
case 2:
return mobile.replaceAll("(\\d{3})(\\d{4})(\\d{4})", "$1$2****");
default:
return mobile;
}
}
public static String idNumberEncrypt(String idNumber) {
if (StrUtil.isEmpty(idNumber)) {
return idNumber;
}
return filterString(idNumber, SYMBOL_STAR);
}
public static String emailEncrypt(String email) {
if (StrUtil.isEmpty(email)) {
return email;
}
return filterEmail(email, SYMBOL_STAR);
}
public static String addressEncrypt(String address) {
if (StrUtil.isEmpty(address)) {
return address;
}
return filterAddress(address, SYMBOL_STAR);
}
public static String filterAddress(String address, String symbol) {
if (StrUtil.isNotBlank(address) && !address.contains("省") && !address.contains("市")) {
return buildSymbol(symbol, address.length());
}
return address;
}
public static String filterEmail(String email, String symbol) {
if (email.contains("@")) {
int i = email.lastIndexOf("@");
String front = email.substring(0, i);
String rear = email.substring(i).replace("@", "#");
return filterString(front, symbol).concat(rear);
}
return email;
}
public static String filterString(String s, String symbol) {
if (StrUtil.isNotBlank(s)) {
char[] charArray = s.toCharArray();
switch (logMode) {
//隐藏前三位
case 0:
for (int i = 0; i < Math.min(3, charArray.length); i++) {
charArray[i] = symbol.charAt(0);
}
return String.valueOf(charArray);
//隐藏中间4位
case 1:
for (int i = Math.max((charArray.length / 2 - 2), 0); i < (Math.min((charArray.length / 2 + 2), charArray.length)); i++) {
charArray[i] = symbol.charAt(0);
}
return String.valueOf(charArray);
case 2:
for (int i = Math.max((charArray.length - 1), 0); i >= (Math.max((charArray.length - 4), 0)); i--) {
charArray[i] = symbol.charAt(0);
}
return String.valueOf(charArray);
default:
break;
}
}
return s;
}
/**
* @param symbol 符号
* @param num 数量
* @return 创建一定数量的相同符号字符串
*/
public static String buildSymbol(String symbol, int num) {
if (num < 0) {
throw new IndexOutOfBoundsException("-1");
}
StringBuilder value = new StringBuilder();
while (--num >= 0) {
value.append(symbol);
}
return value.toString();
}
}
备注: logMode 与 logSensitive 参数 可配置在启动参数上,然后springboot项目启动时读取入参并赋值给对应的静态变量以达到日志规则与日志开关的动态控制。
2.xml中配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<property name="APP_NAME" value="data"/>
<property name="LOG_HOME" value="./data/logs/${APP_NAME}"/>
<conversionRule conversionWord="m" converterClass="....SensitiveLogDataConverter"/>
......
</configuration>