做为金融业务开发,很多接口都需要使用到用户信息,而在用户信息当中难免会有一些敏感字段,比如:用户姓名,银行卡号等等。所以在用户敏感信息保存以及日志打印的时候就不能把这些敏感信息明文的保存起来。对于数据库保存用户敏感信息的时候,一般系统中会有一个加/解密的系统。当需要保存用户敏感信息的时候会把用户信息进行加密,然后保存到数据库当中。这个不属于本文讨论的范畴,而且在保存在数据库当中的时候建议保存格式如下:
用户身份证号:511911202005281234
加密后的数据:P1234567
保存在DB的数据:P1234567:511******1234
有可能在对比用户信息的时候会使用到掩码信息,看会员信息的真实身份证号是否能够对应得上。
下面我们就来讨论系统当中,敏感日志打印问题。
当我们需要打印日志的时候,一般会使用两种方式进行日志的打印。
而且在日志处理的时候一般会使用以下两种处理方式:
***
。*
号。比如:511******1234
。基于以上的考虑我们就来实现它。
标注注解,表示对象中这个字段的日志可以忽略。
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Ignore {
}
标注注解,表示对象中这个字段的日志影响日志的查询,可以使用正则方式打印。
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Mask {
String pattern() default "";
}
实现 common-lang3 中的 ReflectionToStringBuilder,通过反射调用打印对象当中的日志。重写了 ReflectionToStringBuilder#appendFieldsIn
方法来实现我们的打印效果。
public class AnnotationToStringBuilder extends ReflectionToStringBuilder {
public AnnotationToStringBuilder(Object object) {
super(object);
}
public AnnotationToStringBuilder(Object object, ToStringStyle style) {
super(object, style);
}
public AnnotationToStringBuilder(Object object, ToStringStyle style, StringBuffer buffer) {
super(object, style, buffer);
}
public <T> AnnotationToStringBuilder(T object, ToStringStyle style, StringBuffer buffer, Class<? super T> reflectUpToClass, boolean outputTransients, boolean outputStatics) {
super(object, style, buffer, reflectUpToClass, outputTransients, outputStatics);
}
public <T> AnnotationToStringBuilder(T object, ToStringStyle style, StringBuffer buffer, Class<? super T> reflectUpToClass, boolean outputTransients, boolean outputStatics, boolean excludeNullValues) {
super(object, style, buffer, reflectUpToClass, outputTransients, outputStatics, excludeNullValues);
}
@Override
protected void appendFieldsIn(Class clazz) {
if (clazz.isArray()) {
this.reflectionAppendArray(this.getObject());
return;
}
Field[] fields = clazz.getDeclaredFields();
AccessibleObject.setAccessible(fields, true);
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
String fieldName = field.getName();
if (this.accept(field)) {
try {
Object fieldValue = this.getValue(field);
Mask mask = AnnotationUtils.getAnnotation(field, Mask.class);
if( (fieldValue instanceof String) && mask != null && StringUtils.isNotBlank(mask.pattern())) {
String value = String.class.cast(fieldValue);
String pattern = mask.pattern();
this.append(fieldName, OutMaskUtil.replaceWithMask(pattern, value));
continue;
}
Ignore ignore = AnnotationUtils.getAnnotation(field, Ignore.class);
if(ignore != null) {
if(fieldValue != null) {
this.append(fieldName, "***");
} else {
this.append(fieldName, "null");
}
continue;
}
this.append(fieldName, fieldValue);
} catch (IllegalAccessException ex) {
throw new InternalError("Unexpected IllegalAccessException: " + ex.getMessage());
}
}
}
}
}
这种方式需要实现 Object 的 toString 方法。比如:
@Data
public class UserInfo {
/** 用户名称 */
private String username;
/** 身份证号 */
@Mask(pattern = "[\\w]{5}([\\w]*)[\\w]{3}")
private String idNo;
/** 用户地址 */
@Ignore
private String address;
@Override
public String toString(){
return new AnnotationToStringBuilder(this).toString();
}
}
在这种情况下存在两种情况,一种是这个对象是一个 java bean 对象,另外一种就是 JSONObject。它们都可以通过实现 fastjson 提供的 com.alibaba.fastjson.serializer.ValueFilter
来进行处理。对于 java bean 可以通过反射处理,处理 JSONObject 就需要进行特殊处理,不能通用化。
public class LoggerJSON {
static final SerializeConfig SERIALIZE_CONFIG;
static final MaskFilter MASK_FILTER;
static final Map<String, MaskStrategy> MASK_FIELDS;
static {
SERIALIZE_CONFIG = new SerializeConfig();
MASK_FILTER = new MaskFilter();
MASK_FIELDS = buildMaskConfig();
}
/**
* 掩码配置
*/
public static Map<String, MaskStrategy> buildMaskConfig() {
Map<String, MaskStrategy> result = new HashMap<>();
result.put("idNo", new NullMaskStrategy());
return result;
}
public static <T> String toMaskJsonString(T content) {
return JSON.toJSONString(content, SERIALIZE_CONFIG, MASK_FILTER);
}
static class MaskFilter implements ValueFilter {
@Override
public Object process(Object object, String name, Object value) {
if(!isString(value)){
return value;
}
String stringValue = String.class.cast(value);
Class<?> clazz = object.getClass();
try {
// JSONObject 指定处理字段
if(MASK_FIELDS.containsKey(name)){
MaskStrategy maskStrategy = MASK_FIELDS.get(name);
return maskStrategy.process(stringValue);
}
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
// Java Bean 标注 @Ignore 注解
Ignore ignore = AnnotationUtils.getAnnotation(field, Ignore.class);
if(ignore != null && value != null) {
return "***";
}
// Java Bean 标注 @Mask 注解
Mask mask = AnnotationUtils.getAnnotation(field, Mask.class);
if(mask != null && StringUtils.isNotBlank(mask.pattern())) {
return OutMaskUtil.replaceWithMask(mask.pattern(), stringValue);
}
} catch (Exception e) {
// ignore
}
return value;
}
private boolean isString(Object value){
if(value == null) {
return false;
}
return value instanceof String;
}
}
}
对于 java bean 还是使用原来的 @Ignore 和 @Mask 注解进行处理。对于 JSONObject 的时候,需要处理的字段就需要手动的添加进来,并且可以指定日志打印策略。策略接口如下:
public interface MaskStrategy {
String process(String value);
}
这里的 idNo 使用的是打印空策略,也就是不打印它。
public class NullMaskStrategy implements MaskStrategy {
@Override
public String process(String value) {
return null;
}
}
使用 JSON 方式进行日志脱敏就不需要重写 Object 对象的 toString 方法。