Java基于logback?MessageConverter實現(xiàn)日志脫敏方案分析
背景簡介
日志脫敏 是常見的安全需求,最近公司也需要將這一塊內(nèi)容進行推進??戳艘蝗W(wǎng)上的案例,很少有既輕量又好用的輪子可以讓我直接使用。我一直是反對過度設(shè)計的,而同樣我認為輪子就應(yīng)該是可以讓人拿去直接用的。所以我準備分享兩篇博客分別實現(xiàn)兩種日志脫敏方案。
方案分析
- logback MessageConverter + 正則匹配本篇博客主要介紹此方法
- 優(yōu)勢
- 侵入性低、工作量極少, 只需要修改xml配置文件,適合老項目
- 劣勢
- 效率低,會對每一行日志都進行正則匹配檢查,效率受日志長度影響,日志越長效率越低,影響日志吞吐量
- 因基于正則匹配 存在錯殺風險,部分內(nèi)容難以準確識別
- 劣勢
- 侵入性低、工作量極少, 只需要修改xml配置文件,適合老項目
- 優(yōu)勢
- fastjson Filter + 注解 + 工具類下一篇博客介紹
- 優(yōu)勢
- 性能損耗低、效率高、擴展性強,精準脫敏,適合QPS較高日志吞吐量較大的項目。
- 劣勢
- 侵入性較高,需對所有可能的情況進行脫敏判斷
- 存在漏殺風險,全靠開發(fā)控制
- 優(yōu)勢
其實還有一種方案,基于 工具類+配置模式
優(yōu)勢是 工作量低(比注解模式低,比正則匹配模式高),靈活度高,性能也好。但是只適合那些新項目,如果是老項目大家命名不規(guī)范,就很難推動整改了。此處不進行擴展。詳見:項目日志脫敏
logback MessageConverter + 正則匹配
流程圖解

代碼案例
正則匹配日志脫敏工具類
此工具類主要用于實現(xiàn)依據(jù)配置的正則匹配規(guī)則集,進行依次匹配。并提取敏感文本對其執(zhí)行對應(yīng)的脫敏策略。大家拿去用可以不做修改
package com.zhibo.log.format;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @Author: Zhibo.lv
* @Description: 正則匹配日志脫敏工具類
**/
@Component
public class LogSensitiveUtils {
// 脫敏日志最大長度,超出此長度的日志放棄脫敏,直接返回
private static Integer SENSITIVE_LOG_MAX_LENGTH = 10000;
/**
* 日志脫敏 獲取規(guī)則集進行依次匹配
* @param content 明文日志文本
* @return 脫敏后的日志文本
*/
public static String filterSensitive(String content) {
try {
if (StringUtils.isNotBlank(content) && content.length() < SENSITIVE_LOG_MAX_LENGTH) {
for (Map.Entry<String, List<Pattern>> entry : LogSensitiveConstants.SENSITIVE_SEQUENCE.entrySet()) {
content = filter(content, entry.getKey(), entry.getValue());
}
}
return content;
} catch (Exception e) {
return content;
}
}
/**
*
* @param content 需脫敏字符串
* @param type 文本類型,依據(jù)類型可以做不同的脫敏方式
* @param patterns 該方式下需匹配的正則
* @return
*
*/
public static String filter(String content, String type, List<Pattern> patterns) {
for (Pattern pattern : patterns) {
Matcher matcher = pattern.matcher(content);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, Matcher.quoteReplacement(baseSensitive(matcher.group(), type)));
}
matcher.appendTail(sb);
content = sb.toString();
}
return content;
}
/**
* 依據(jù)正則抓去的文本執(zhí)行對應(yīng)的脫敏策略
* @param str 待脫敏的字符串
* @return
*/
private static String baseSensitive(String str, String type) {
if (StringUtils.isBlank(str)) {
return StringUtils.EMPTY;
}
//通過工廠獲取對應(yīng)類型的脫敏類執(zhí)行脫敏方法
return SensitiveStrategyBuiltInUtil.getStrategy(type).des(str);
}
}正則匹配日志脫敏常量
此工具類主要是進行配置需要脫敏的文本的正則。需要大家依據(jù)業(yè)務(wù)調(diào)整或新增
package com.zhibo.log.format;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Pattern;
/**
* @Author: Zhibo
* @Description: 正則匹配日志脫敏常量
**/
public class LogSensitiveConstants {
/**
* 過濾先后順序:身份證 -> 手機號
* 順序原因:避免部分業(yè)務(wù)需求出現(xiàn)可能同時滿足多個正則規(guī)則的文本,大家可以優(yōu)先提取更長的、更復(fù)雜的文本。后處理簡單的
*/
public static final Map<String,List<Pattern>> SENSITIVE_SEQUENCE = new TreeMap<String, List<Pattern>>();
/**
* 手機號匹配規(guī)則集,支持配置多個正則規(guī)則
*/
public static final List<Pattern> SENSITIVE_PHONE_KEY = new ArrayList<Pattern>(1);
/**
* 身份證號碼匹配規(guī)則集,支持配置多個正則規(guī)則
*/
public static final List<Pattern> SENSITIVE_ID_NO_KEY = new ArrayList<Pattern>(1);
/**
* 手機號正則匹配,11位1開頭數(shù)字
* 瞻前顧后:校驗符合要求的文本前后均不能為數(shù)字 避免誤匹配
*/
public static final String PHONE_REGEX = "(?<!\\d)[1][3-9][0-9]{9}(?!\\d)";
/**
* 身份證號正則匹配 18位數(shù)版本
* 15位數(shù)的身份證號碼暫不考慮,如果需要自行新增下方正則加入 SENSITIVE_ID_NO_KEY 中
* (?<!\d)([1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3})(?!\d)
* 瞻前顧后:校驗符合要求的文本前后均不能為數(shù)字 避免誤匹配
*/
public static final String ID_NO_REGEX = "(?<!\\d)([1-9]\\d{5}[1-9]\\d{3}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}([0-9]|X))(?!\\d)";
static {
SENSITIVE_ID_NO_KEY.add(Pattern.compile(ID_NO_REGEX));
SENSITIVE_PHONE_KEY.add(Pattern.compile(PHONE_REGEX));
}
// 脫敏替代字符
public static final char STAR = '*';
// 手機號類型脫敏替代字符
public static final String PHONE_MASK = "****";
/** 手機號碼脫敏策略 */
public static final String STRATEGY_PHONE = "strategyPhone";
/** 身份證號碼脫敏策略 */
public static final String STRATEGY_ID_NO = "strategyIdNo";
static {
//將每一個規(guī)則集綁定一個對應(yīng)的類型
SENSITIVE_SEQUENCE.put(STRATEGY_ID_NO, SENSITIVE_ID_NO_KEY);
SENSITIVE_SEQUENCE.put(STRATEGY_PHONE, SENSITIVE_PHONE_KEY);
}
private LogSensitiveConstants() {
}
}脫敏策略代碼
定義文本脫敏接口 IStrategy
package com.zhibo.log.sensitive.api;
/**
* @Author: Zhibo
* @Description: 脫敏策略
*/
public interface IStrategy {
/**
* 脫敏
* @param original 原始內(nèi)容
* @return 脫敏后的字符串
*/
String des(final Object original);
}文本脫敏抽象類,進行通用實現(xiàn) AbstractStringStrategy
package com.zhibo.log.sensitive.core.strategory;
import com.zhibo.log.sensitive.api.IStrategy;
import com.zhibo.log.format.LogSensitiveConstants;
import java.security.MessageDigest;
/**
* @Author: zhibo
* @Description: 抽象字符串策略,
* 支持在脫敏后的文本后面追加明文的MD5加密串,方便研發(fā)進行日志查詢使用
*/
public abstract class AbstractStringStrategy implements IStrategy {
/**
* 獲取掩碼之前的長度
* @param original 原始
* @param chars 字符串
* @return 結(jié)果
*/
protected abstract int getBeforeMaskLen(Object original, char[] chars);
/**
* 獲取掩碼之后的長度
* @param original 原始
* @param chars 字符串
* @return 結(jié)果
*/
protected abstract int getAfterMaskLen(Object original, char[] chars);
/**
* 針對固定長度的加密直接返回脫敏字符串,避免StringBuilder循環(huán)拼接
* @return 脫敏字符串
* 如返回null 則通過 {@link AbstractStringStrategy#getBeforeMaskLen(Object, char[])} 與 {@link AbstractStringStrategy#getAfterMaskLen(Object, char[])}
* 進行截取字符串
*/
protected String getMask(){
return null;
}
/**
* 是否需要拼接MD5密文方便日志查詢。
* @return false : 不拼接(默認)
* true : 拼接密文 用于日志查詢 格式 [MD5]
*/
protected Boolean addMD5(){
return false;
}
@Override
public String des(Object original) {
if(original == null) {
return null;
}
String strValue = original.toString();
char[] chars = strValue.toCharArray();
int beforeMaskLen = getBeforeMaskLen(original, chars);
int afterMaskLen = getAfterMaskLen(original, chars);
//范圍糾正
int maxLen = chars.length;
beforeMaskLen = Math.min(beforeMaskLen, maxLen);
afterMaskLen = Math.min(afterMaskLen, maxLen);
StringBuilder stringBuilder = new StringBuilder();
//獲取明文前綴
if(beforeMaskLen > 0) {
stringBuilder.append(chars, 0, beforeMaskLen);
}
//獲取脫敏字符串
String mask = getMask();
if (null == mask){//如未指定脫敏字符串則按規(guī)則循環(huán)拼接
// 中間使用掩碼
for(int i = beforeMaskLen; i < chars.length - afterMaskLen; i++) {
stringBuilder.append(LogSensitiveConstants.STAR);
}
}else {
stringBuilder.append(mask);
}
//獲取明文后綴
if(afterMaskLen > 0) {
stringBuilder.append(chars, chars.length - afterMaskLen, afterMaskLen);
}
if (addMD5()){
addMD5(strValue,stringBuilder);
}
return stringBuilder.toString();
}
// MD5加密
private void addMD5(String originalString,StringBuilder stringBuilder) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(originalString.getBytes());
byte[] digest = md.digest();
stringBuilder.append("[");
for (byte b : digest) {
stringBuilder.append(String.format("%02x", b));
}
stringBuilder.append("]");
} catch (Exception e) {
e.printStackTrace();
}
}
}身份證脫敏策略實現(xiàn) StrategyIdNo
package com.zhibo.log.sensitive.core.strategory;
/**
* @Author: Zhibo
* @Description: 身份證號脫敏
* 脫敏規(guī)則:保留前6 后4 位,其它由星號替換
*/
public class StrategyIdNo extends AbstractStringStrategy {
@Override
protected int getBeforeMaskLen(Object original, char[] chars) {
return 6;
}
@Override
protected int getAfterMaskLen(Object original, char[] chars) {
return 4;
}
}手機號碼脫敏策略實現(xiàn) StrategyPhone
package com.zhibo.log.sensitive.core.strategory;
import com.zhibo.log.format.LogSensitiveConstants;
/**
* @Author: zhibo
* @Description: 手機號脫敏
* 脫敏規(guī)則:186****8567[MD5]
*/
public class StrategyPhone extends AbstractStringStrategy {
@Override
protected int getBeforeMaskLen(Object original, char[] chars) {
return 3;
}
@Override
protected int getAfterMaskLen(Object original, char[] chars) {
return 4;
}
@Override
protected String getMask() {
return LogSensitiveConstants.PHONE_MASK;
}
@Override
protected Boolean addMD5(){
return true;
}
}logback 消息轉(zhuǎn)換器實現(xiàn)
最關(guān)鍵的方法來啦
package com.zhibo.log.format;
import ch.qos.logback.classic.pattern.MessageConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
/**
* @Author: Zhibo
* @Description: 日志脫敏轉(zhuǎn)換器
**/
public class SensitiveConverter extends MessageConverter {
@Override
public String convert(ILoggingEvent event){
// 獲取原始日志
String requestLogMsg = super.convert(event);
// 執(zhí)行日志脫敏
return LogSensitiveUtils.filterSensitive(requestLogMsg);
}
public SensitiveConverter() {
}
}自此我們的工具包也就完成了,業(yè)務(wù)系統(tǒng)需要使用此工具只需要修改resources目錄下的logback.xml配置。
<!-- 新增或修改原有消息轉(zhuǎn)換器為SensitiveConverter --> <conversionRule conversionWord="msgToo" converterClass="com.zhibo.log.format.SensitiveConverter" />
并將文件輸出日志的消息內(nèi)容替換為指定消息轉(zhuǎn)換器的 conversionWord

脫敏效果展示

有請?zhí)崾?/h3>
注:此方法對日志吞吐量存在影響,由于正則需要循環(huán)匹配整個日志文本,所以正則規(guī)則越多,日志文本越長,耗時越長。如您的應(yīng)用程序?qū)θ罩就掏铝恳筝^高且存在大量超長日志文本請壓測后使用。
如配置了logback的異步打印,且設(shè)置了允許日志丟棄,在壓測中可能出現(xiàn)因線程池與等待隊列均被占滿而導(dǎo)致日志丟失情況。下面是我的問題復(fù)盤:
logback日志異步打印配置如下
<appender name="ASYNC-FILE" class="ch.qos.logback.classic.AsyncAppender"> <neverBlock>true</neverBlock><!-- 非阻塞方式運行 如隊列滿就開始丟棄日志 --> <queueSize>1024</queueSize><!-- 等待隊列大小 --> <discardingThreshold>0</discardingThreshold><!-- 日志隊列深度,配置0 隊列滿后丟棄最老的日志 --> <appender-ref ref="FILE"/> </appender>
以上配置 為logback線程池工作配置,默認線程池 線程數(shù)為 10個,最大隊列長度為1024個。
意味著如果日志產(chǎn)生的速度超過10個線程工作處理日志的速度,則無法處理的日志會被寫入BlockingQueue 隊列,當隊列滿了之后就會導(dǎo)致日志丟失的情況。
到此這篇關(guān)于Java基于logback MessageConverter實現(xiàn)日志脫敏的文章就介紹到這了,更多相關(guān)java logback MessageConverter日志脫敏內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JDBC PreparedStatement Like參數(shù)報錯解決方案
這篇文章主要介紹了JDBC PreparedStatement Like參數(shù)報錯解決方案,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習或者工作具有一定的參考學(xué)習價值,需要的朋友可以參考下2020-10-10
Mybatis之@ResultMap,@Results,@Result注解的使用
這篇文章主要介紹了Mybatis之@ResultMap,@Results,@Result注解的使用,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12
詳解使用Spring Cloud Consul實現(xiàn)服務(wù)的注冊和發(fā)現(xiàn)
這篇文章主要介紹了詳解使用Spring Cloud Consul實現(xiàn)服務(wù)的注冊和發(fā)現(xiàn),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-06-06

