文章目录
一、一些敏感信息比如 手机号、身份证号,不在明文在日志中打印,自定义logback转换器,将这些字段脱敏后输出
1、定义一个ESensitiveDataConverter 类如下:
import ch.qos.logback.classic.pattern.MessageConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import org.apache.commons.lang3.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ESensitiveDataConverter extends MessageConverter {
/**
* 日志脱敏开关
*/
private static String converterCanRun = "true";
/**
* 日志脱敏关键字
*/
private static String sensitiveDataKeys = "email,mobile,idCard,bankNo";
private static Pattern pattern = Pattern.compile("[0-9a-zA-Z]");
@Override
public String convert(ILoggingEvent event){
// 获取原始日志
String oriLogMsg = event.getFormattedMessage();
// 获取脱敏后的日志
String afterLogMsg = invokeMsg(oriLogMsg);
return afterLogMsg;
}
/**
* 处理日志字符串,返回脱敏后的字符串
* @return
*/
public String invokeMsg(final String oriMsg){
String tempMsg = oriMsg;
if("true".equals(converterCanRun)){
// 处理字符串
if(sensitiveDataKeys != null && sensitiveDataKeys.length() > 0){
String[] keysArray = sensitiveDataKeys.split(",");
for(String key: keysArray){
int index= -1;
do{
index = tempMsg.indexOf(key, index+1);
if(index != -1){
// 判断key是否为单词字符
if(isWordChar(tempMsg, key, index)){
continue;
}
// 寻找值的开始位置
int valueStart = getValueStartIndex(tempMsg, index + key.length());
// 查找值的结束位置(逗号,分号)........................
int valueEnd = getValuEndEIndex(tempMsg, valueStart);
// 对获取的值进行脱敏
String subStr = tempMsg.substring(valueStart, valueEnd);
subStr = tuomin(subStr, key);
///
tempMsg = tempMsg.substring(0,valueStart) + subStr + tempMsg.substring(valueEnd);
}
}while(index != -1);
}
}
}
return tempMsg;
}
private String tuomin(String submsg, String key){
// idcard:身份证号, name:姓名, bankcard:银行卡号, mobile:手机号
if(!StringUtils.isEmpty(submsg)){
return submsg.substring(0,1)+"******";
}else{
return "******";
}
}
/**
* 判断从字符串msg获取的key值是否为单词 , index为key在msg中的索引值
* @return
*/
private boolean isWordChar(String msg, String key, int index){
// 必须确定key是一个单词............................
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;
}
/**
* 获取value值的开始位置
* @param msg 要查找的字符串
* @param valueStart 查找的开始位置
* @return
*/
private int getValueStartIndex(String msg, int valueStart ){
// 寻找值的开始位置.................................
do{
char ch = msg.charAt(valueStart);
if(ch == ':' || ch == '='){ // key与 value的分隔符
valueStart ++;
ch = msg.charAt(valueStart);
if(ch == '"'){
valueStart ++;
}
break; // 找到值的开始位置
}else{
valueStart ++;
}
}while(true);
return valueStart;
}
/**
* 获取value值的结束位置
* @return
*/
private int getValuEndEIndex(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 == ','){
// 去掉前面的 \ 处理这种形式的数据
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;
}
}
2、logback.xml中添加如下配置:
注意conversionRule 配置的位置,必须在前,否则不生效
<conversionRule conversionWord="msg" converterClass="com.test.encrypt.ESensitiveDataConverter"> </conversionRule>
无需再添加其它Jar引入,利用logback的Converter,自定义日志格式转换符,然后继承ClassicConverter就可以了 。
可以看到这里日志脱敏是通过 解析格式化后的日志来 查找需要脱敏的关键字,然后脱敏关键字后面的字符串来实现。这种方法很慢。那么还可以重写ConsoleAppender
二、重新定义一个ConsoleAppender
1、logback.xml中原本或许是这样
ConsoleAppender是logback中定义的
<!-- 管控台日志打印,发布生产需注释 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder charset="utf-8">
<Pattern>[%date{yyyy-MM-dd HH:mm:ss.SSS}] %level [%X{TRACE_LOG_ID}] [%file:%method:%line] %msg%n</Pattern>
</encoder>
</appender>
2、重新定义一个Appender继承ConsoleAppender
@Slf4j
public class SensitiveConsoleAppender extends ConsoleAppender {
@Override
protected void subAppend(Object event) {
try {
//处理log.info("xxx:{}",arg) 日志中的参数
this.DealArg((LoggingEvent) event);
}catch (Exception e){
log.error("log error:{}!", Throwables.getStackTraceAsString(e));
}finally {
//这里最终需要调用父类的subAppend,让日志往下走
super.subAppend(event);
}
}
private void DealArg(LoggingEvent event){
//获取到参数,这里注意,只能处理对象类型的参数
//比如:log.info("用户信息:{}",user)
//处理不了log.info("邮箱",email);这种类型的。因为这里片是脱敏是 通过判断对象的属性是否是需要脱敏的字段 来决定的
Object[] args = event.getArgumentArray();
if(!Objects.isNull(args)){
for(int i =0; i<args.length; i++){
Object arg = args[i];
//对象中需要脱敏的字段,脱敏。
String s = MaskUtil.mask(arg);
args[i] = s;
}
//event.setArgumentArray(rargs);
}
}
}
3、脱敏方法的注意
log.info("用户信息:{}",user);
//注意调用上面打印日志后,不应改变原对象user的值,因为如果打印user这个对象的日志后,再用user去处理其它业务逻辑 ,如果得到的user对象是脱敏后的对象 那么不对。 如果脱敏的方法不应改变user对象本身。
比如你可以这样:
public static String toJSONString(Object o){
try{
String t = JSON.toJSONString(o);
Object target = JSON.parseObject(t,o.getClass());
return MaskUtil.deal(o);
}catch (Exception e){
log.info("MaskUtilerror:{}", Throwables.getStackTraceAsString(e));
return JSON.toJSONString(o);
}
}
可能上述方法会比较慢,大致思路是这样的,就是处理脱敏时 需要深拷贝一份,不应该直接操作原始参数。
4、配置
<!-- 管控台日志打印,发布生产需注释 -->
<appender name="STDOUT" class="com.test.log.SensitiveConsoleAppender">
<encoder charset="utf-8">
<Pattern>[%date{yyyy-MM-dd HH:mm:ss.SSS}] %level [%X{TRACE_LOG_ID}] [%file:%method:%line] %msg%n</Pattern>
</encoder>
</appender>
三、重新定义一个Filter实现
1、重新定一个filter
@Slf4j
public class SampleFilter extends Filter<ILoggingEvent> {
@Override
public FilterReply decide(ILoggingEvent event) {
try {
this.maskDeal((LoggingEvent) event);
}catch (Exception e){
log.error("log error:{}!", Throwables.getStackTraceAsString(e));
}finally {
return FilterReply.ACCEPT;
}
}
private void maskDeal(LoggingEvent event){
Object[] args = event.getArgumentArray();
if(!Objects.isNull(args)){
for(int i =0; i<args.length; i++){
Object arg = args[i];
String s = MaskUtil.deal(arg);
args[i] = s;
}
//event.setArgumentArray(rargs);
}
}
}
2、xml定义
这时不用重写Appenderr,只需要原来Appender的添加一个filter即可。
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder charset="utf-8">
<Pattern>[%date{yyyy-MM-dd HH:mm:ss.SSS}] %level [%X{TRACE_LOG_ID}] [%file:%method:%line] %msg%n</Pattern>
</encoder>
<filter class="com.test.log.filter.SampleFilter"/>
</appender>
测试方法:
public static void tests_log(){
Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
LoggerContext loggerContext = rootLogger.getLoggerContext();
// we are not interested in auto-configuration
loggerContext.reset();
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setContext(loggerContext);
encoder.setPattern("%-5level [%thread]: %message%n");
encoder.start();
ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<ILoggingEvent>();
appender.setContext(loggerContext);
appender.setEncoder(encoder);
appender.start();
rootLogger.addAppender(appender);
SampleFilter sampleFilter = new SampleFilter();
appender.addFilter(sampleFilter);
}