1. 程式人生 > >資料脫敏——基於Java自定義註解實現日誌欄位脫敏

資料脫敏——基於Java自定義註解實現日誌欄位脫敏



  上文說了資料過敏主要有兩個思路:第一個就是在序列化實體之前先把需要脫敏的欄位進行處理,之後正常序列化;第二個就是在實體序列化的時候,對要脫敏的欄位進行處理。


脫敏實現思路

  這裡探討第一種方法,用基於自定義註解的方式實現日誌脫敏。

  要對資料進行脫敏,基本上都是對一些關鍵的、少數字段進行脫敏,比如某個實體中可能只對password這一個欄位進行脫敏處理,所以可以用自定義註解的方式,只需在需要脫敏的欄位上新增一個註解,比較方便。

  整體思路如下圖:

這裡寫圖片描述

  寫日誌時,序列化之前先把要列印的物件clone一份,然後找出新增脫敏自定義註解的欄位進行相應規則的處理轉化(比如把“劉德華”改為“劉*華),然後再對物件進行序列化操作。

核心程式碼:

定義用於標識脫敏欄位的註解 Desensitized.java

@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Desensitized {
    /*脫敏型別(規則)*/
    SensitiveTypeEnum type();
    /*判斷註解是否生效的方法*/
    String isEffictiveMethod() default "";
}

脫敏型別 SensitiveTypeEnum.java

public enum SensitiveTypeEnum {
    /** 中文名 */
    CHINESE_NAME,
    /** 身份證號 */
    ID_CARD,
    /** 座機號 */
    FIXED_PHONE,
    /** 手機號 */
    MOBILE_PHONE,
    /** 地址 */
    ADDRESS,
    /** 電子郵件 */
    EMAIL,
    /** 銀行卡 */
    BANK_CARD,
    /** 密碼 */
    PASSWORD;
}

實現脫敏處理類 DesensitizedUtils.java

public
class DesensitizedUtils { /** * 獲取脫敏json串 * * @param javaBean * @return */ public static String getJson(Object javaBean) { String json = null; if (null != javaBean) { try { if (javaBean.getClass().isInterface()) return json; /* 克隆出一個實體進行欄位修改,避免修改原實體 */ Object clone = ObjectUtils.deepClone(javaBean); /* 定義一個計數器,用於避免重複迴圈自定義物件型別的欄位 */ Set<Integer> referenceCounter = new HashSet<Integer>(); /* 對克隆實體進行脫敏操作 */ DesensitizedUtils.replace(ObjectUtils.getAllFields(clone), clone, referenceCounter); /* 利用fastjson對脫敏後的克隆物件進行序列化 */ json = JSON.toJSONString(clone, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullListAsEmpty); /* 清空計數器 */ referenceCounter.clear(); referenceCounter = null; } catch (Throwable e) { e.printStackTrace(); } } return json; } /** * 對需要脫敏的欄位進行轉化 * * @param fields * @param javaBean * @param referenceCounter * @throws IllegalArgumentException * @throws IllegalAccessException */ private static void replace(Field[] fields, Object javaBean, Set<Integer> referenceCounter) throws IllegalArgumentException, IllegalAccessException { if (null != fields && fields.length > 0) { for (Field field : fields) { field.setAccessible(true); if (null != field && null != javaBean) { Object value = field.get(javaBean); if (null != value) { Class<?> type = value.getClass(); //處理子屬性,包括集合中的 if (type.isArray()) {//對陣列型別的欄位進行遞迴過濾 int len = Array.getLength(value); for (int i = 0; i < len; i++) { Object arrayObject = Array.get(value, i); if (isNotGeneralType(arrayObject.getClass(), arrayObject, referenceCounter)) { replace(ObjectUtils.getAllFields(arrayObject), arrayObject, referenceCounter); } } } else if (value instanceof Collection<?>) {//對集合型別的欄位進行遞迴過濾 Collection<?> c = (Collection<?>) value; Iterator<?> it = c.iterator(); while (it.hasNext()) { Object collectionObj = it.next(); if (isNotGeneralType(collectionObj.getClass(), collectionObj, referenceCounter)) { replace(ObjectUtils.getAllFields(collectionObj), collectionObj, referenceCounter); } } } else if (value instanceof Map<?, ?>) {//對Map型別的欄位進行遞迴過濾 Map<?, ?> m = (Map<?, ?>) value; Set<?> set = m.entrySet(); for (Object o : set) { Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o; Object mapVal = entry.getValue(); if (isNotGeneralType(mapVal.getClass(), mapVal, referenceCounter)) { replace(ObjectUtils.getAllFields(mapVal), mapVal, referenceCounter); } } } else if (value instanceof Enum<?>) { continue; } /*除基礎型別、jdk型別的欄位之外,對其他型別的欄位進行遞迴過濾*/ else { if (!type.isPrimitive() && type.getPackage() != null && !StringUtils.startsWith(type.getPackage().getName(), "javax.") && !StringUtils.startsWith(type.getPackage().getName(), "java.") && !StringUtils.startsWith(field.getType().getName(), "javax.") && !StringUtils.startsWith(field.getName(), "java.") && referenceCounter.add(value.hashCode())) { replace(ObjectUtils.getAllFields(value), value, referenceCounter); } } } //脫敏操作 setNewValueForField(javaBean, field, value); } } } } /** * 脫敏操作(按照規則轉化需要脫敏的欄位並設定新值) * 目前只支援String型別的欄位,如需要其他型別如BigDecimal、Date等型別,可以新增 * * @param javaBean * @param field * @param value * @throws IllegalAccessException */ public static void setNewValueForField(Object javaBean, Field field, Object value) throws IllegalAccessException { //處理自身的屬性 Desensitized annotation = field.getAnnotation(Desensitized.class); if (field.getType().equals(String.class) && null != annotation && executeIsEffictiveMethod(javaBean, annotation)) { String valueStr = (String) value; if (StringUtils.isNotBlank(valueStr)) { switch (annotation.type()) { case CHINESE_NAME: { field.set(javaBean, DesensitizedUtils.chineseName(valueStr)); break; } case ID_CARD: { field.set(javaBean, DesensitizedUtils.idCardNum(valueStr)); break; } case FIXED_PHONE: { field.set(javaBean, DesensitizedUtils.fixedPhone(valueStr)); break; } case MOBILE_PHONE: { field.set(javaBean, DesensitizedUtils.mobilePhone(valueStr)); break; } case ADDRESS: { field.set(javaBean, DesensitizedUtils.address(valueStr, 8)); break; } case EMAIL: { field.set(javaBean, DesensitizedUtils.email(valueStr)); break; } case BANK_CARD: { field.set(javaBean, DesensitizedUtils.bankCard(valueStr)); break; } case PASSWORD: { field.set(javaBean, DesensitizedUtils.password(valueStr)); break; } } } } } }

脫敏測試物件 UserInfo.java

public class UserInfo{
    @Desensitized(type = SensitiveTypeEnum.CHINESE_NAME)
    private String realName;

    @Desensitized(type = SensitiveTypeEnum.ID_CARD)
    private String idCardNo;

    @Desensitized(type = SensitiveTypeEnum.MOBILE_PHONE)
    private String mobileNo;

    private String account;

    @Desensitized(type = SensitiveTypeEnum.PASSWORD, isEffictiveMethod = "isEffictiveMethod")
    private String password;

    //setter、getter略
}

測試:

@Test
public void testUserInfoDesensitize() {
    BaseUserInfo baseUserInfo = new BaseUserInfo()
                .setRealName("胡丹尼")
                .setIdCardNo("158199199013141120")
                .setMobileNo("13579246810")
                .setAccount("dannyhoo123456")
                .setPassword("123456");
    System.out.println("脫敏前:" + JSON.toJSONString(baseUserInfo));
    System.out.println("脫敏後:" + DesensitizedUtils.getJson(baseUserInfo));
}

  整個過程比較棘手的地方就是物件的克隆,實際場景中要列印的日誌物件格式千變萬化,物件的變數型別也很多,比如介面、列舉、集合、map、自定義型別等,在實現過程中也嘗試了多種方法來實現實體的深克隆,比如先序列化物件,再反序列化得到克隆後的物件,或者用第三方克隆工具類,都沒有很好地相容實際環境中的物件格式,上述原始碼中是小編自己按照現有需求、和出現了許多錯誤後一遍一遍修改來的,可能會有很多不合理的地方,時間緊迫,後面繼續優化。

  針對整個實現的思路、實現方法,如果您有任何疑問和建議,歡迎交流討論。如果您有更好的方法,也希望您能夠分享下~