利用责任链模式及AOP对日志进行脱敏分享
现在公司需要对日志进行脱敏,对一些敏感的值需要掩码处理,已保护用户的数据的安全,对脱敏的字段包括姓名、身份证号码、手机号码、银行账号、邮箱等。
思路如下:
1、如何匹配这些字段,想到的是有两种,
一种是对字段名的判断,这种有可能会误伤一些字段,但考虑到只是对字段的掩码并且只是日志,误伤也应该没事,
另外一种是对字段的值用正则表达式判断
2、对每个字段都要过滤,那么每一个字段都要过滤各种规则,应该说每种规则都要过滤每一个字段,知道符合某一种规则,
这里的规则指的就是符合姓名、身份证等的字段或者正则表达式,这个很容易想到struts2的拦截器,所以我打算用责任链模式把这些规则串连起来,并且如果有新的规则能够自动添加过来,即形成了链条式的过滤器。
3、对这些过滤器加入到spring容器中,并形成一个bean,对那些需要脱敏的日志,只要把这个bean加入就好,并调用这个过滤器。
4、把这个过滤器封装到AOP中,写一个注解,对要日志脱敏的方法入参和出参进行脱敏。这样不用组合这个过滤器。
相关代码如下:
1、先定义过滤的字段和值
package com.shux.brop.nbi.common.filter;
/**
* 描述:
* @author Simba
* @date 2018年7月24日
*/
public class Paramter {
private String key;//需要过滤字段的名字
private String value;//需要过滤字段的值
public Paramter(String key, String value) {
super();
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
2、定义过滤器
package com.shux.brop.nbi.common.filter;
import com.shux.brop.nbi.common.filter.impl.FilterChain;
/**
* 描述:
* @author Simba
* @date 2018年7月24日
*/
public interface Filter {
void doFilter(Paramter param,FilterChain chain);
}
3、定义FilterChain,这个主要把所有的filter整合在一起,一个个迭代执行,它本身也是一个filter
package com.shux.brop.nbi.common.filter.impl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
import com.shux.brop.nbi.common.filter.Filter;
import com.shux.brop.nbi.common.filter.Paramter;
/**
* 描述:
* @author Simba
* @date 2018年7月24日
*/
@Service("filterChain")
public class FilterChain implements Filter,ApplicationContextAware,InitializingBean {
private List<Filter> filters = new ArrayList<Filter>();
private ApplicationContext context;
private int index = 0;
public FilterChain addFilter(Filter filter) {
this.filters.add(filter);
return this;
}
@Override
public void doFilter(Paramter param,FilterChain chain) {
if (index >= filters.size()) {
return;
}
Filter filter = filters.get(index);
index ++;
filter.doFilter(param, chain);
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
@Override
public void afterPropertiesSet() throws Exception {
Map<String, Filter> filterMap = context.getBeansOfType(Filter.class);
Collection<Filter> allFilters = filterMap.values();
for (Filter filter : allFilters) {
if (filter instanceof FilterChain) {
continue;
}
filters.add(filter);//把所有的filter加入进来
}
}
@Override
public void setApplicationContext(ApplicationContext context)
throws BeansException {
this.context = context;
}
}
4、把所有的filter抽象出一个父类,只要继承这个类并且加入到spring容器就认为是责任链中的一个链条.
package com.shux.brop.nbi.common.filter.impl;
import com.shux.brop.nbi.common.filter.Filter;
import com.shux.brop.nbi.common.filter.Paramter;
/**
* 描述:
* @author Simba
* @date 2018年7月25日
*/
public abstract class AbstractFilter implements Filter {
@Override
public void doFilter(Paramter param, FilterChain chain) {
if (doFilter(param)) {
return;
}
chain.doFilter(param, chain);
}
/**
* 如果符合对应的脱敏逻辑则返回true,否则返回false
* @param param 包含key和value,指字段名和对应的值,可以根据字段名或值进行脱敏
* @return
*/
protected abstract boolean doFilter(Paramter param);
}
5、对敏感字段脱敏的几个规则,即链条
package com.shux.brop.nbi.common.filter.impl;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;
import com.shux.brop.nbi.common.filter.Paramter;
/**
* 描述:生日脱敏,只要字段名含有BIRTHDAY关键字,则需要脱敏
* @author Simba
* @date 2018年7月25日
*/
@Component
public class BirthdayFilter extends AbstractFilter {
@Override
protected boolean doFilter(Paramter param) {
String key = param.getKey();
if (StringUtils.isNotEmpty(key) && key.toUpperCase().contains("BIRTHDAY")) {
param.setValue("******");
return true;
}
return false;
}
}
package com.shux.brop.nbi.common.filter.impl;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;
import com.shux.brop.nbi.common.filter.Paramter;
/**
* 描述:邮箱脱敏,字段值符合正则表达式,则需要脱敏
* @author Simba
* @date 2018年7月25日
*/
@Component
public class EmailFilter extends AbstractFilter {
private static final String EMAIL_REGEX = "\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*";
@Override
protected boolean doFilter(Paramter param) {
String value = param.getValue();
Pattern pattern = Pattern.compile(EMAIL_REGEX);
Matcher matcher = pattern.matcher(value);
if (StringUtils.isNotEmpty(value) && matcher.matches()) {
param.setValue("******" + value.substring(value.indexOf("@")));
}
return false;
}
}
6、把整个过滤器封装到spring中,可以对String、List,Object及嵌套bean等进行脱敏。
package com.shux.brop.nbi.common.service;
/**
* 描述:日志过敏处理
* @author Simba
* @date 2018年7月30日
*/
public interface ISensitivityMessageFilter {
String doSensitivityFilter(Object param);
}
package com.shux.brop.nbi.common.service;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.shux.brop.nbi.common.filter.Paramter;
import com.shux.brop.nbi.common.filter.impl.FilterChain;
/**
* 描述:
* @author Simba
* @date 2018年7月30日
*/
@Service
public class SensitivityMessageFilterImpl implements ISensitivityMessageFilter {
@Autowired
@Qualifier("filterChain")
private FilterChain filterChain;
@Value("${common.logger.entrypt.log.flag:true}")
private String entryptLog;
private final Logger logger = LoggerFactory.getLogger(getClass());
@Override
public String doSensitivityFilter(Object param) {
/*if (logger.isDebugEnabled()) {
logger.debug(" the source param is :{}",JSON.toJSONString(param));
}*/
try {
if (param == null) {
return null;
}
if (!Boolean.valueOf(entryptLog)) {//如果脱敏开关关闭的话就不脱敏
return JSON.toJSONString(param);
}
FilterChain fc = filterChain;
StringBuilder result = new StringBuilder();
fc.setIndex(0);
if (param instanceof String || param instanceof Enum) {
Paramter paramter = new Paramter("key", String.valueOf(param));
fc.doFilter(paramter, fc);
result.append("\"");
result.append(paramter.getValue());
result.append("\"");
return result.toString();
} else if (objectInstanceofNotStringBaseType(param)) {
return String.valueOf(param);
} else if (param instanceof List || param instanceof Collection) {
String listParam = JSON.toJSONString(param);
JSONArray parseArray = JSON.parseArray(listParam);
Iterator<Object> iterator = parseArray.iterator();
result.append("[");
int i = 0;
while (iterator.hasNext()) {
Object object = iterator.next();
result.append(doSensitivityFilter(object));//list里包含了对象,对象需重新过来敏感字符
if (i < parseArray.size() - 1) {
result.append(",");
}
i++;
}
result.append("]");
return result.toString();
}
String jsonParam = JSON.toJSONString(param);
JSONObject parseObject = JSON.parseObject(jsonParam);
Set<Entry<String, Object>> entrySet = parseObject.entrySet();
int index = 0;
for (Entry<String, Object> entry : entrySet) {
fc.setIndex(0);
Object object = entry.getValue();//对象里套了对象的情况
if (index == 0) {
result.append("{");
}
result.append("\"").append(entry.getKey()).append("\"")
.append(":");
if (objectInstanceofBaseType(object)) {//基本类型的时候过滤
Paramter paramter = new Paramter(entry.getKey(),
String.valueOf(object));
if (object instanceof String) {
fc.doFilter(paramter, fc);
result.append("\"").append(paramter.getValue())
.append("\"");
} else {
result.append(paramter.getValue());
}
} else {
result.append(doSensitivityFilter(object));
}
if (index >= entrySet.size() - 1) {
result.append("}");
} else {
result.append(",");
}
index++;
}
return result.toString();
} catch (Exception e) {
logger.error("---------------print log error------------",e);
}
return null;
}
private boolean objectInstanceofBaseType(Object object) {
if (object instanceof String || objectInstanceofNotStringBaseType(object)) {
return true;
}
return false;
}
private boolean objectInstanceofNotStringBaseType(Object object) {
if (object instanceof Integer || object instanceof BigDecimal || object instanceof Double || object instanceof Boolean || object instanceof Date || object instanceof Long) {
return true;
}
return false;
}
}
7、把过滤器加入到AOP中,通过注解的形式对方法入参和出参进行脱敏处理,注意要在配置文件中加入<aop:aspectj-autoproxy proxy-target-class="true" />标签
package com.shux.brop.nbi.common.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import com.shux.brop.nbi.common.enums.Position;
/**
* 描述:用于记录方法的入参和出参的日志
* 要用此注解,必须在组件的spring配置文件中开启AOP开关,
* 在组件中加入以下标签
* <aop:aspectj-autoproxy proxy-target-class="true" />
* @author Simba
* @date 2018年7月12日
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CommonLogger {
Position[] positions();
}
package com.shux.brop.nbi.common.enums;
/**
* 描述:打印方法入参和出参的标识枚举
* PARAMTER 表示打印入参
* RESULT 表示打印出参
* @author Simba
* @date 2018年7月12日
*/
public enum Position {
PARAMTER,
RESULT;
}
package com.shux.brop.nbi.common.aspect;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.stereotype.Component;
import com.shux.brop.nbi.common.annotation.CommonLogger;
import com.shux.brop.nbi.common.annotation.ESAPrintLogFlag;
import com.shux.brop.nbi.common.enums.Position;
import com.shux.brop.nbi.common.service.ISensitivityMessageFilter;
import com.shux.brop.nbi.common.util.MethodUtil;
import com.shux.pafa.papp.ESA;
/**
* 描述:日志注解切面类,含对敏感字符的的过滤
* @author Simba
* @date 2018年7月12日
*/
@Aspect
@Component
public class LoggerAspectBean {
private final Logger log = LoggerFactory.getLogger(getClass());
@Autowired
private ISensitivityMessageFilter sensitivityMessageFilter;
@Pointcut(value = "@annotation(com.shux.brop.nbi.common.annotation.CommonLogger)")
private void pointcut() {
}
@Around(value = "pointcut() && @annotation(logger)")
public Object printLog(ProceedingJoinPoint joinPoint,CommonLogger logger) throws Throwable {
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
final Method method = signature.getMethod();
final Object [] param = joinPoint.getArgs();
if (Arrays.asList(logger.positions()).contains(Position.PARAMTER)) {
try {
printParamLog(method, param);
} catch (Exception e) {
String className = method.getDeclaringClass().getName();
log.error("<<<<<<<<<<<<<< print param of {} error",className + "." +method.getName());
log.error("<<<<<<<<<<<<<< the reason is :", e);
}
}
Object result = null;
try {
result = joinPoint.proceed();
} catch (Throwable e) {
log.error("execute target method error,the reason is :",e);
throw e;
}
if (Arrays.asList(logger.positions()).contains(Position.RESULT)) {
try {
printResultLog(method, result);
} catch (Exception e) {
String className = method.getDeclaringClass().getName();
log.error("<<<<<<<<<<<<<< print result of {} error",className + "." + method.getName());
log.error("<<<<<<<<<<<<<< the reason is :", e);
}
}
return result;
}
private void printParamLog(Method method, Object[] param) {
StringBuilder paramLog = new StringBuilder();
String className = method.getDeclaringClass().getName();
paramLog.append("the params of ")
.append(className)
.append(".").append(method.getName())
.append(" is :");//.append(System.getProperty("line.separator"));
if (param == null || param.length == 0) {
paramLog.append("null");
}
else {
List<String> paramterNames = getMethodParamter(method);
for (int i = 0; i < param.length; i++) {
if (paramterNames != null && StringUtils.isNotEmpty(paramterNames.get(i))) {
paramLog.append(paramterNames.get(i));
} else {
paramLog.append("param" + (i+1));
}
paramLog.append(":").append(doSensitivityFilter(param[i])).append(",");
}
}
log.info(paramLog.toString());
}
private void printResultLog(Method method,Object result) {
StringBuilder resultLog = new StringBuilder();
String className = method.getDeclaringClass().getName();
resultLog.append("the result of ")
.append(className)
.append(".").append(method.getName())
.append(" is :");//.append("\r\n");
if (result instanceof Void) {
resultLog.append(" void");
} else {
resultLog.append(doSensitivityFilter(result));
}
log.info(resultLog.toString());
}
private List<String> getMethodParamter(Method method) {
ParameterNameDiscoverer parameterNameDiscoverer = MethodUtil.getInstance().newParamterNameDiscoverer();
String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);
if (parameterNames == null || parameterNames.length == 0) {
return null;
}
return Arrays.asList(parameterNames);
}
/**
* 执行敏感字段过滤器
* @param param
* @return
*/
private String doSensitivityFilter(Object param) {
return sensitivityMessageFilter.doSensitivityFilter(param);
}
}
8、示例
通过注解的形式脱敏
package com.shux.brop.nbi.trade.online.service;
import com.shux.brop.nbi.trade.online.dto.OrderDetailDTO;
/**
* 描述:
* @author Simba
* @date 2018年7月25日
*/
public interface LogFilterService {
void log(OrderDetailDTO dto,String str);
}
package com.shux.brop.nbi.trade.online.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.shux.brop.nbi.common.annotation.CommonLogger;
import com.shux.brop.nbi.common.enums.Position;
import com.shux.brop.nbi.common.service.ISensitivityMessageFilter;
import com.shux.brop.nbi.trade.online.dto.OrderDetailDTO;
/**
* 描述:
* @author Simba
* @date 2018年7月25日
*/
@Service
public class LogFilterServiceImpl implements LogFilterService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
@CommonLogger(positions={Position.PARAMTER})
public void log(OrderDetailDTO dto,String str) {
logger.info("--------------------------------LogFilterServiceImpl---------------------------");
}
}
package com.shux.brop.nbi.trade.online.service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import com.shux.brop.nbi.trade.online.dto.NbiCustomerInfoDTO;
import com.shux.brop.nbi.trade.online.dto.OrderDetailDTO;
import com.shux.pafa.papp.test.BaseSARTest;
import com.shux.pafa.papp.test.SARContextConfiguration;
/**
* 描述:
* @author Simba
* @date 2018年7月25日
*/
@RunWith(SpringJUnit4ClassRunner.class)
public class LogFilterServiceTest {
@Autowired
private LogFilterService logFilterService;
@Test
public void testLogFilter() {
OrderDetailDTO dto = new OrderDetailDTO();
NbiCustomerInfoDTO custom = new NbiCustomerInfoDTO();
custom.setBirthday("2016-02-25");
custom.setDetailAddress("广东生深圳市罗湖区大剧院京基100大厦");
custom.setEmail("12355@qq.com");
custom.setCardId("440515199212237208");
custom.setMobileNo("13698545691");
custom.setName("张山疯");
NbiCustomerInfoDTO custom1 = new NbiCustomerInfoDTO();
custom1.setBirthday("20160225");
custom1.setDetailAddress("广东生深圳市罗湖区大剧院京基100大厦");
custom1.setEmail("12355@qq.com");
custom1.setCardId("440515199212237208");
custom1.setMobileNo("13326985424");
custom1.setName("张山疯");
dto.setApplyPolicyNo("12365998");
dto.setCurrentSysDate(new Date());
dto.setOrderNo("IC201807130849120300010000001");
dto.setPolicyHolder(custom);
List<NbiCustomerInfoDTO> list = new ArrayList<NbiCustomerInfoDTO>();
list.add(custom);
list.add(custom1);
dto.setCustomerInfoList(list);
dto.setOrderStatus(2);
logFilterService.log(dto,"440515199212237208");
}
}
日志打印结果如下:
[16:11:34.685] [INFO] [<T=U0MMZeivc00k3UQg>] LoggerAspectBean: the params of com.shux.brop.nbi.trade.online.service.LogFilterServiceImpl.log is :dto:{"orderNo":"IC201807130849120300010000001","currentSysDate":1536048694641,"policyHolder":{"cardId":"4****************8","birthday":"******","email":"******@qq.com","name":"*山疯","mobileNo":"1369****691","sortNumber":0,"detailAddress":"广东生深圳市罗湖区******"},"orderStatus":2,"customerInfoList":[{"cardId":"4****************8","birthday":"******","email":"******@qq.com","name":"*山疯","sortNumber":0,"mobileNo":"1369****691","detailAddress":"广东生深圳市罗湖区******"},{"cardId":"4****************8","birthday":"******","email":"******@qq.com","name":"*山疯","sortNumber":0,"mobileNo":"1332****424","detailAddress":"广东生深圳市罗湖区******"}],"applyPolicyNo":"12365998"},str:"4****************8"
数据已经脱敏处理
通过注入的形式脱敏
package com.shux.brop.nbi.trade.online.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.shux.brop.nbi.common.annotation.CommonLogger;
import com.shux.brop.nbi.common.enums.Position;
import com.shux.brop.nbi.common.service.ISensitivityMessageFilter;
import com.shux.brop.nbi.trade.online.dto.OrderDetailDTO;
/**
* 描述:
* @author Simba
* @date 2018年7月25日
*/
@Service
public class LogFilterServiceImpl implements LogFilterService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ISensitivityMessageFilter sensitivityMessageFilter;
@Override
public void log(OrderDetailDTO dto,String str) {
logger.debug(sensitivityMessageFilter.doSensitivityFilter(dto));
logger.info("--------------------------------LogFilterServiceImpl---------------------------");
}
}
运行以上测试用例,打印日志如下
[16:14:19.003] [DEBUG] [<T=J0MMZfKqw00kqMvp>] LogFilterServiceImpl: {"orderNo":"IC201807130849120300010000001","currentSysDate":1536048858953,"policyHolder":{"cardId":"4****************8","birthday":"******","email":"******@qq.com","name":"*山疯","mobileNo":"1369****691","sortNumber":0,"detailAddress":"广东生深圳市罗湖区******"},"orderStatus":2,"customerInfoList":[{"cardId":"4****************8","birthday":"******","email":"******@qq.com","name":"*山疯","sortNumber":0,"mobileNo":"1369****691","detailAddress":"广东生深圳市罗湖区******"},{"cardId":"4****************8","birthday":"******","email":"******@qq.com","name":"*山疯","sortNumber":0,"mobileNo":"1332****424","detailAddress":"广东生深圳市罗湖区******"}],"applyPolicyNo":"12365998"}
日志已经成功脱敏
至此,日志脱敏已经成功完成.