logback 日志脱敏处理
1.按正则表达式脱敏处理
参考:
https://www.cnblogs.com/htyj/p/12095615.html
http://www.heartthinkdo.com/?p=998
站在两位创作者的肩膀上,我很不要脸的将他们的内容做了下整合,捂脸中...
一般处理都是继承PatternLayout实现自己的处理方式,上代码
注意:这里隐藏处理只是针对数字类型的字符串做了简单的编码替换处理,可用其他通用加密方式进行替代。
package com.demo.log; import ch.qos.logback.classic.PatternLayout; import ch.qos.logback.classic.spi.ILoggingEvent; import org.apache.commons.lang.StringUtils; import org.springframework.util.CollectionUtils; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 对敏感信息进行掩盖。 * 1.实现原理 * 对产生的日志信息,进行正则匹配和替换。 * <p> * 2.目前包括如下类型的信息:银行卡号、电话、身份证和邮箱。 * <p> * 3.如何进行扩展新的正则类型 * (1)在PatternType枚举中新增一个正则 * (2)extractMatchesByType对新增的正则做处理 * (3)maskByType对新增的正则做处理 * <p> */ public class MaskingPatternLayout extends PatternLayout { /** * 匹配的所有正则 */ private Map<PatternType, Pattern> patternsMap = new HashMap<>(); private static final String KEY = "GepqwLZYdk"; public MaskingPatternLayout() { loadPatterns(); } @Override public String doLayout(ILoggingEvent event) { String message = super.doLayout(event); if (CollectionUtils.isEmpty(patternsMap)) { return message; } // 处理日志信息 try { return process(message); } catch (Exception e) { // 这里不做任何操作,直接返回原来message return message; } } /** * 加载正则表达式,生成相应的Pattern对象。 */ private void loadPatterns() { for (PatternType patternType : PatternType.values()) { Pattern pattern = Pattern.compile(patternType.getRegex()); patternsMap.put(patternType, pattern); } } /** * 替换信息 * * @param message * @return */ public String process(String message) { for (PatternType key : patternsMap.keySet()) { // 1.生成matcher Pattern pattern = patternsMap.get(key); Matcher matcher = pattern.matcher(message); // 2.获取匹配的信息 Set<String> matches = extractMatchesByType(matcher); // 3.掩盖匹配的信息 if (!CollectionUtils.isEmpty(matches)) { message = maskByType(key, message, matches); } } return message; } /** * 根据正则类型来做相应的提取 * * @param matcher * @return */ private Set<String> extractMatchesByType(Matcher matcher) { // 邮箱、电话、银行卡、身份证都是通过如下方法进行提取匹配的字符串 return extractDefault(matcher); } /** * 1.提取匹配的所有字符串中某一个分组 * group(0):表示不分组,整个表达式的值 * group(i),i>0:表示某一个分组的值 * <p> * 2.使用Set进行去重 * * @param matcher * @return */ private Set<String> extractDefault(Matcher matcher) { Set<String> matches = new HashSet<>(); int count = matcher.groupCount(); while (matcher.find()) { if (count == 0) { matches.add(matcher.group()); continue; } for (int i = 1; i <= count; i++) { String match = matcher.group(i); if (null != match) { matches.add(match); } } } return matches; } /** * 根据不同类型敏感信息做相应的处理 * * @param key * @param message * @return */ private String maskByType(PatternType key, String message, Set<String> matchs) { if (key == PatternType.ID_CARD) { return maskIdCard(message, matchs); } else if(key == PatternType.BANK_CARD){ return maskBankcard(message, matchs); } else if(key == PatternType.PHONE_NUMBER){ return maskPhone(message, matchs); } else{ return message; } } /** * 掩盖数字类型信息 * * @param message * @param matches * @return */ private String maskIdCard(String message, Set<String> matches) { for (String match : matches) { // 1.处理获取的字符 String matchProcess = baseSensitive(match, 4, 4); // 2.String的替换 message = message.replace(match, matchProcess); } return message; } private String maskBankcard(String message, Set<String> matches) { for (String match : matches) { // 1.处理获取的字符 String matchProcess = baseSensitive(match, 3, 3); // 2.String的替换 message = message.replace(match, matchProcess); } return message; } private String maskPhone(String message, Set<String> matches) { for (String match : matches) { // 1.处理获取的字符 String matchProcess = baseSensitive(match, 2, 2); // 2.String的替换 message = message.replace(match, matchProcess); } return message; } private static String baseSensitive(String str, int startLength, int endLength) { if (StringUtils.isBlank(str)) { return ""; } String replacement = str.substring(startLength,str.length()-endLength); StringBuffer sb = new StringBuffer(); for(int i=0;i<replacement.length();i++) { char ch; if(replacement.charAt(i)>='0' && replacement.charAt(i)<='9') { ch = KEY.charAt((int)(replacement.charAt(i) - '0')); }else { ch = replacement.charAt(i); } sb.append(ch); } return StringUtils.left(str, startLength).concat(StringUtils.leftPad(StringUtils.right(str, endLength), str.length() - startLength, sb.toString())); } private static String decrypt(String str, int startLength, int endLength) { if (StringUtils.isBlank(str)) { return ""; } String replacement = str.substring(startLength,str.length()-endLength); StringBuffer sb = new StringBuffer(); for(int i=0;i<replacement.length();i++) { int index = KEY.indexOf(replacement.charAt(i)); if(index != -1) { sb.append(index); }else { sb.append(replacement.charAt(i)); } } return StringUtils.left(str, startLength).concat(StringUtils.leftPad(StringUtils.right(str, endLength), str.length() - startLength, sb.toString())); } /** * 定义敏感信息类型 */ private enum PatternType { // 1.手机号共11位,模式为: 13xxx,,14xxx,15xxx,17xxx,18xx PHONE_NUMBER("手机号", "[^\\d](1[34578]\\d{9})[^\\d]"), // 2.银行卡号,包含16位和19位 BANK_CARD("银行卡", "[^\\d](\\d{16})[^\\d]|[^\\d](\\d{19})[^\\d]"), // 3.邮箱 EMAIL("邮箱", "[A-Za-z_0-9]{1,64}@[A-Za-z1-9_-]+.[A-Za-z]{2,10}"), // 4. 15位(全为数字位)或者18位身份证(17位位数字位,最后一位位校验位) ID_CARD("身份证", "[^\\d](\\d{15})[^\\d]|[^\\d](\\d{18})[^\\d]|[^\\d](\\d{17}X)"); private String description; private String regex; private PatternType(String description, String regex) { this.description = description; this.regex = regex; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getRegex() { return regex; } public void setRegex(String regex) { this.regex = regex; } } }
logback.xml:
<property name="rolling.pattern" value="%d{yyyy-MM-dd}"/> <property name="layout.pattern" value="%-5p %d [%t] %c{50} > %m%n"/> <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder"> <layout class="com.demo.log.MaskingPatternLayout"> <pattern>${layout.pattern}</pattern> </layout> </encoder> </appender>
2.按指定字段脱敏处理
参考:https://gitee.com/cqdevops/diary_desensitization
注意:这种方式是需要一定前提条件的,日志内容的格式有限制(如json串或者{字段名=“”}),具体可以到参考文章看看,然后可以在源码的基础上自己调整。
说明一下,这里是指cardId跟idNo这两者的字段名的内容按idCardNo类型处理,realName字段名的内容按照trueName方式处理,一开始我也看得云里雾里。
下载源码后,导入工程后,maven install到本地仓库,不能直接使用install后的jar,因为它没有把依赖包打进去,引用的话会报ClassNotFound
在你maven工程下的pom.xml引用,文章中引用的groupId是错误的,所以会一直引不到:
<dependency> <groupId>com.gitee.cqdevops</groupId> <artifactId>desensitization-logback</artifactId> <version>1.1.1</version> </dependency>
针对源码做了一些微调,对字段内容开始的tag做了一下处理,但可能不是最优的处理:
package com.gitee.cqdevops.desensitization.pattern; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; public class KeywordConverter extends BaseConverter { private static Pattern pattern = Pattern.compile("[0-9a-zA-Z]"); @Override public String invokeMsg(final String oriMsg){ String tempMsg = oriMsg; try { if("true".equals(converterCanRun)){ if(!keywordMap.isEmpty()){ Set<String> keysArray = keywordMap.keySet(); for(String key: keysArray){ int index = -1; int i = 0; do{ index = tempMsg.indexOf(key, index + 1); if(index != -1){ if(isWordChar(tempMsg, key, index)){ continue; } Map<String,Object> valueStartMap = getValueStartIndex(tempMsg, index + key.length()); int valueStart = (int)valueStartMap.get("valueStart"); char tag = (char)valueStartMap.get("tag"); int valueEnd = getValueEndEIndex(tempMsg, valueStart,tag); // 对获取的值进行脱敏 String subStr = tempMsg.substring(valueStart, valueEnd); subStr = facade(subStr, keywordMap.get(key)); tempMsg = tempMsg.substring(0,valueStart) + subStr + tempMsg.substring(valueEnd); i++; } }while(index != -1 && i < depth); } } } } catch (Exception e) { return tempMsg; } return tempMsg; } /** * 判断key是否为单词内字符 * @param msg 待检查字符串 * @param key 关键字 * @param index 起始位置 * @return 判断结果 */ private boolean isWordChar(String msg, String key, int index){ if(index != 0){ // 判断key前面一个字符 char preCh = msg.charAt(index-1); Matcher match = pattern.matcher(preCh + ""); if(match.matches()){ return true; } } // 判断key后面一个字符 char nextCh = msg.charAt(index + key.length()); Matcher match = pattern.matcher(nextCh + ""); if(match.matches()){ return true; } return false; } private Map<String,Object> getValueStartIndex(String msg, int valueStart ){ Map<String,Object> map= new HashMap<>(); do{ char ch = msg.charAt(valueStart); if(ch == ':' || ch == '='){ valueStart ++; ch = msg.charAt(valueStart); if(ch == '"' || ch =='\''){ valueStart ++; map.put("valueStart",valueStart); map.put("tag",ch); } break; }else{ valueStart ++; } }while(true); return map; } private int getValueEndEIndex(String msg, int valueEnd,char tag){ do{ if(valueEnd == msg.length()){ break; } char ch = msg.charAt(valueEnd); if(ch == tag){ if(valueEnd + 1 == msg.length()){ break; } char nextCh = msg.charAt(valueEnd + 1); if(nextCh == ';' || nextCh == ','|| nextCh == '}'){ while(valueEnd > 0 ){ char preCh = msg.charAt(valueEnd - 1); if(preCh != '\\'){ break; } valueEnd--; } break; }else{ valueEnd ++; } } else{ valueEnd ++; } }while(true); return valueEnd; } /** * 寻找key对应值的开始位置 * @param msg 待检查字符串 * @param valueStart 开始寻找位置 * @return key对应值的开始位置 */ // private int getValueStartIndex(String msg, int valueStart ){ // do{ // char ch = msg.charAt(valueStart); // if(ch == ':' || ch == '='){ // valueStart ++; // ch = msg.charAt(valueStart); // if(ch == '"'){ // valueStart ++; // } // break; // }else{ // valueStart ++; // } // }while(true); // // return valueStart; // } /** * 寻找key对应值的结束位置 * @param msg 待检查字符串 * @param valueEnd 开始寻找位置 * @return key对应值的结束位置 */ private int getValueEndEIndex(String msg, int valueEnd){ do{ if(valueEnd == msg.length()){ break; } char ch = msg.charAt(valueEnd); if(ch == '"'){ if(valueEnd + 1 == msg.length()){ break; } char nextCh = msg.charAt(valueEnd + 1); if(nextCh == ';' || nextCh == ','|| nextCh == '}'){ while(valueEnd > 0 ){ char preCh = msg.charAt(valueEnd - 1); if(preCh != '\\'){ break; } valueEnd--; } break; }else{ valueEnd ++; } }else if (ch ==';' || ch == ',' || ch == '}'){ break; }else{ valueEnd ++; } }while(true); return valueEnd; } }