大家项目中肯定都会用到日志打印,目的是为了以后线上排查问题方便,但是有些企业对输出的日志包含的敏感(比如:用户身份证号,银行卡号,手机号等)信息要进行脱敏处理。
哎!我们最近就遇到了日志脱敏的改造。可以说是一波三折。
浩浩荡荡的日志脱敏改造就这样开始..........
第一季 :在项目中所有log.info方法输出日志的地方手工脱敏,很low的一种方法
初期想法很简单,就是根据业务,判断哪些info输出的地方可能包含敏感信息,然后进行手工脱敏。但是有很大弊端,一是工作量大,二是业务不熟悉很难一次改全。
脱敏工具类:
java
package com.lsl.utills;
public class Utils {
public static String checkParams(String str){
if (str != null){
String strnew = str.replaceAll("1[3-9]\\d{9}","***");
return strnew;
}else {
return "";
}
}
}
代码中调用这个工具类:
java
package com.lsl.controller;
import com.lsl.utills.Utils;
import com.lsl.vo.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/lsl")
public class LogbackTestController {
private static final Logger LOG = LoggerFactory.getLogger(LogbackTestController.class);
@PostMapping(value = "qryUser", produces = "application/json;charset=UTF-8")
@ResponseBody
public String qryUser(){
User user = new User();
user.setUserName("LSL");
user.setUserPwd("123@asd");
user.setUserAddr("北京海淀");
user.setUserAge(21);
user.setUserTel("15803125588");
LOG.info("用户信息:{}",user.toString());
LOG.info("脱敏后的用户信息:{}", Utils.checkParams(user.toString()));
return "success";
}
}
结果截图:
这种情况确实也能达到脱敏的目的,但是如果代码中有好多info日志输出,那么都需要改动,工作量太大,还容易拉下没有改动的地方。其实我们初期就是这种脱敏,反复改了好几次,总有遗漏的地方忘记改了。
第二季:想研究一种省时省力的方式,能不能通过AOP切入log.info方法进行统一脱敏
大概思路,就是创建一个切面,在调用info方法前把打印内容通过上面的工具类脱敏后在打印。
切面:
java
package com.lsl.config;
import com.lsl.utills.Utils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class LogInfoAspect {
@Pointcut("execution(* org.slf4j.Logger.info(..))")
public void loginfoPointcut(){
}
@Before("loginfoPointcut() && args(msg)")
public void bfLogInfo(ProceedingJoinPoint joinPoint,String msg) throws Throwable{
System.out.println("切入前参数msg=" + msg);
String newStr = Utils.checkParams(msg);
System.out.println("脱敏处理后参数=" + newStr);
joinPoint.proceed(new Object[]{newStr});
}
@Around("loginfoPointcut() && args(msg)")
public void adLogInfo(ProceedingJoinPoint joinPoint,String msg) throws Throwable{
// String arg = (String)joinPoint.getArgs()[0];
System.out.println("切入前参数msg=" + msg);
String newStr = Utils.checkParams(msg);
System.out.println("脱敏处理后参数=" + newStr);
joinPoint.proceed(new Object[]{newStr});
}
}
结果是:没法切入到info方法,也就无法脱敏了
如下图:
原因也很简单:AOP是交付spring托管的bean才能使用动态代理,实现切入进行操作。然后logback并没有由spring托管,所以不行。
我后来想到,在配置类里交付spring托管不就行了,但是也不行,大家猜猜是什么原因呢?
第三季:利用logback脱敏并实现异步写入日志文件
大概思路是,需要自己写脱敏转换器,然后引入logback.xml中,异步写入文件比较方便,网上好多资料,大家可以自行查询。
脱敏转换器:
java
package com.lsl.config;
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 SensitiveConverter extends MessageConverter {
//手机号正则
private static final String PHONE_REG = "(1([358][0-9]|4[579]|66|7[0135678]|9[89])[0-9]{8}|[96]\\d{7}|6[68]\\d{5}|09\\d{8})([^@^\\d]|$)";
//身份证号正则
private static final String IDCARD_REG = "";
//脱敏字符
private static final String SECRET = "***************";
//要脱敏的类型
private static String sensitiveType;
public static void setSensitiveType(String sensitiveType){
SensitiveConverter.sensitiveType = sensitiveType;
}
@Override
public String convert(ILoggingEvent event){
String requestLogMsg = event.getFormattedMessage();
return filtrSensitive(requestLogMsg,sensitiveType);
}
public String filtrSensitive(String content,String sensitiveType){
try {
if (StringUtils.isBlank(content)){
return content;
}
//手机号脱敏
if (sensitiveType.contains("phone")){
content = filterPhone(content);
}
//身份证脱敏
if (sensitiveType.contains("idcard")){
}
return content;
} catch (Exception e) {
return content;
}
}
private static String filterPhone(String num){
Pattern pattern = Pattern.compile(PHONE_REG);
Matcher matcher = pattern.matcher(num);
StringBuffer sb = new StringBuffer();
while (matcher.find()){
String matchResult = matcher.group();
if (!StringUtils.isNumeric(matchResult.substring(matchResult.length() - 1))){
//大陆手机号
if (matchResult.startsWith("1")){
matcher.appendReplacement(sb,baseSensitive(matchResult,3,5));
}
//港澳手机号
if (matchResult.startsWith("6") || matchResult.startsWith("9")){
}
}else {
//大陆手机号
if (matchResult.startsWith("1")){
matcher.appendReplacement(sb,baseSensitive(matchResult,3,4));
}
//港澳手机号
if (matchResult.startsWith("6") || matchResult.startsWith("9")){
}
}
}
matcher.appendTail(sb);
return sb.toString();
}
private static String baseSensitive(String str,int startlen,int endlen){
if (StringUtils.isBlank(str)){
return "";
}
StringBuffer sb = new StringBuffer();
sb.append(SECRET);
return StringUtils.left(str,startlen).concat(StringUtils.leftPad(StringUtils.right(str,endlen),
str.length()- startlen,sb.toString()));
}
}
java
package com.lsl.config;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class SensitiveConverterProperties implements InitializingBean {
@Value("${logging.sensitive.type}")
private String sensitiveType;
@Override
public void afterPropertiesSet() throws Exception {
SensitiveConverter.setSensitiveType(this.sensitiveType);
}
public String getSensitiveType() {
return sensitiveType;
}
public void setSensitiveType(String sensitiveType) {
this.sensitiveType = sensitiveType;
}
}
application.properties文件
java
server.port=8080
logging.sensitive.type=phone
logback.xml
java
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!-- 转换配置 必选配置-->
<conversionRule conversionWord="sensitiveMsg" converterClass="com.lsl.config.SensitiveConverter"> </conversionRule>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %sensitiveMsg%n</pattern>
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %sensitiveMsg%n</pattern>
</encoder>
</appender>
<!-- 异步日志配置 -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="ROLLING" />
</appender>
<!-- 项目包下所有info日志输出到控制台 -->
<logger name="com.lsl" level="INFO" additivity="false">
<appender-ref ref="CONSOLE" />
</logger>
<!-- 项目包下所有info日志异步输出到文件 -->
<logger name="com.lsl" level="INFO" additivity="false">
<appender-ref ref="ASYNC_FILE" />
</logger>
<!-- Root Logger -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="ROLLING" />
</root>
</configuration>
controoler验证
java
package com.lsl.controller;
import com.lsl.vo.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/lsl")
public class LogbackTestController {
private static final Logger LOG = LoggerFactory.getLogger(LogbackTestController.class);
@PostMapping(value = "qryUser", produces = "application/json;charset=UTF-8")
@ResponseBody
public String qryUser(){
User user = new User();
user.setUserName("LSL");
user.setUserPwd("123@asd");
user.setUserAddr("北京海淀");
user.setUserAge(21);
user.setUserTel("15803125588");
LOG.info("用户信息:{}",user.toString());
return "success";
}
}
验证截图
总结:
利用logback的脱敏转换器是非常方便且灵活的。自己可以定义各种业务敏感信息的脱敏策略,我这里面只写了手机号的脱敏策略。而且在配置文件properties中可以配置对那些敏感信息进行脱敏。
比如:logging.sensitive.type=phone,idcard
这样就是手机号和身份证号同时进行脱敏,只是具体的身份证号的脱敏规则,我以后在补上吧。