系統日誌中敏感字段掩碼處理

做爲金融業務開發,很多接口都需要使用到用戶信息,而在用戶信息當中難免會有一些敏感字段,比如:用戶姓名,銀行卡號等等。所以在用戶敏感信息保存以及日誌打印的時候就不能把這些敏感信息明文的保存起來。對於數據庫保存用戶敏感信息的時候,一般系統中會有一個加/解密的系統。當需要保存用戶敏感信息的時候會把用戶信息進行加密,然後保存到數據庫當中。這個不屬於本文討論的範疇,而且在保存在數據庫當中的時候建議保存格式如下:

用戶身份證號: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 方法。

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