系统日志中敏感字段掩码处理

做为金融业务开发,很多接口都需要使用到用户信息,而在用户信息当中难免会有一些敏感字段,比如:用户姓名,银行卡号等等。所以在用户敏感信息保存以及日志打印的时候就不能把这些敏感信息明文的保存起来。对于数据库保存用户敏感信息的时候,一般系统中会有一个加/解密的系统。当需要保存用户敏感信息的时候会把用户信息进行加密,然后保存到数据库当中。这个不属于本文讨论的范畴,而且在保存在数据库当中的时候建议保存格式如下:

用户身份证号:511911202005281234
加密后的数据:P1234567
保存在DB的数据:P1234567:511******1234

有可能在对比用户信息的时候会使用到掩码信息,看会员信息的真实身份证号是否能够对应得上。

1、日志打印需要考虑的问题

下面我们就来讨论系统当中,敏感日志打印问题。

当我们需要打印日志的时候,一般会使用两种方式进行日志的打印。

  • 重写 Object 的 toString,打印关注字段的信息
  • 调用 fastjson(或者其它 JSON 处理框架)的 JSON.toString 方法

而且在日志处理的时候一般会使用以下两种处理方式:

  • 忽略日志,当该字段不影响日志查询的时候,可以忽略字段日志。当对象字段为空的时候不打印(或者打印 null 值),当有值时打印 ***
  • 正则打印日志,当该字段影响日志查询的时候,可以使用正则打印日志。比如用户的身份证号,可以在日志当中打印身份证前 3 位,以及后 4 位中间加上 6 个 * 号。比如:511******1234

基于以上的考虑我们就来实现它。

2、对象 toString 打印

2.1 Ignore

标注注解,表示对象中这个字段的日志可以忽略。

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Ignore {
}

2.2 Mask

标注注解,表示对象中这个字段的日志影响日志的查询,可以使用正则方式打印。

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Mask {

    String pattern() default "";

}

2.3 AnnotationToStringBuilder

实现 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();
	}

}

3、JSON 的 toJSONString 方法

在这种情况下存在两种情况,一种是这个对象是一个 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 方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章