自定義注解實現數據序列化時進行數據脫敏


在最近的開發工作中遇到了需要對身份證號碼進行脫敏的操作, 開始的想法特別簡單,就是在數據返回的時候進行數據的脫敏操作,示例代碼如下:

  Page<Reserve> page = PageHelper.startPage(pageNum, pageSize);
        baseMapper.selectList(wrapper);
        //身份證信息脫敏
        List<Reserve> list = page.getResult();
        for (Reserve reserve : list) {
            reserve.setIdCard(PagerUtil.hideIdCard(reserve.getIdCard()));
        }
        pager = PagerUtil.getPager(page, pageNum, pageSize);
        //脫敏后數據賦值
        pager.setList(list);
        return pager;

// 脫敏工具類
    //身份證前三后四脫敏
    public static String hideIdCard(String id) {
        if (StringUtils.isEmpty(id) || (id.length() < 11)) {
            return id;
        }
        return id.replaceAll("(?<=\\w{3})\\w(?=\\w{2})", "*");
    }

優點 :邏輯簡單,理解起來很容易

缺點: 復用性不高, 要在每個需要脫敏的地方復制代碼,當需要的脫敏規則比較多的時候,就需要多個脫敏工具類,不方便維護

后來對上面的代碼進行了優化,網上類似的優化方法有很多,我選擇了自定義注解來實現數據的脫敏(基於springboot的 Jackson),下面就實現的過程進行詳細的描述,步驟如下:

(一)先定義需要的注解

/**
 * 脫敏注解
 *
 * @author wuhuc
 * @data 2022/4/7 - 19:09
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside //這個注解用來標記Jackson復合注解,當你使用多個Jackson注解組合成一個自定義注解時會用到它
@JsonSerialize(using = SensitiveJsonSerializer.class) //指定使用自定義的序列化器
public @interface Sensitive {
    SensitiveStrategy strategy();   //該自定義注解需要的參數   strategy-參數名稱    SensitiveStrategy-參數類型
}

@Retention(RetentionPolicy.RUNTIME) 和 @Target(ElementType.FIELD) 這兩個是元注解,用來標注該注解的使用信息

@Retention(RetentionPolicy.RUNTIME) 表示該注解在運行時生效

@Target(ElementType.FIELD) 表示注解的作用目標 ElementType.FIELD表示注解作用於字段上

@JacksonAnnotationsInside 這個注解用來標記Jackson復合注解,當你使用多個Jackson注解組合成一個自定義注解時會用到它

@JsonSerialize(using = SensitiveJsonSerializer.class) 指定使用自定義的序列化器

SensitiveStrategy strategy(); 該自定義注解需要的參數 strategy-參數名稱 SensitiveStrategy-參數類型

第二步 編寫脫敏的策略的枚舉

/**
 * 校驗數據類型枚舉
 *
 * @author wuhuc
 * @data 2022/4/7 - 19:13
 */
public enum SensitiveStrategy {
    /**
     * Username sensitive strategy.  $1 替換為正則的第一組  $2 替換為正則的第二組
     */
    USERNAME(s -> s.replaceAll("(\\S)\\S(\\S*)", "$1*$2")),
    /**
     * Id card sensitive type.
     */
    ID_CARD(s -> s.replaceAll("(\\d{3})\\d{13}(\\w{2})", "$1****$2")),
    /**
     * Phone sensitive type.
     */
    PHONE(s -> s.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")),
    /**
     * Address sensitive type.
     */
    ADDRESS(s -> s.replaceAll("(\\S{3})\\S{2}(\\S*)\\S{2}", "$1****$2****"));


    private final Function<String, String> desensitizer;

    /**
     * 定義構造函數,傳入一個函數
     */
    SensitiveStrategy(Function<String, String> desensitizer) {
        this.desensitizer = desensitizer;
    }

    /**
     * getter方法
     */
    public Function<String, String> desensitizer() {
        return desensitizer;
    }
}

這個類似一個工廠類,里面放置需要的脫敏策略,需要注意的是這個枚舉返回的是一個函數Function

該函數就是我們定義的脫敏函數,該函數會在后面的序列化時被使用,該枚舉類的注解我寫的很詳細,這里就不一一贅述了

第三步 實現我們的自定義脫敏序列化器

/**
 * 自定義數據脫敏
 *
 * @author wuhuc
 * @data 2022/4/7 - 19:15
 */
public class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {
    private SensitiveStrategy strategy;

    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        //strategy.desensitizer() 返一個Function
        // Function.apply(value) 執行枚舉里面定義的脫敏方法
        gen.writeString(strategy.desensitizer().apply(value));
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
        Sensitive annotation = property.getAnnotation(Sensitive.class);
        if (Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass())) {
            this.strategy = annotation.strategy();
            return this;
        }
        return prov.findValueSerializer(property.getType(), property);
    }
}

JsonSerializer 是需要繼承的序列化方法

ContextualSerializer 是獲取前后文的方法

第四步 使用注解

在需要脫敏的字段上加上注解@Sensitive(strategy = SensitiveStrategy.ID_CARD) 並指定脫敏策略

   /**
     * 預訂人身份證號碼
     */
    @TableField(value = "id_card")
    @Sensitive(strategy = SensitiveStrategy.ID_CARD)
    @ApiModelProperty(value = "預訂人身份證號碼")
    private String idCard;

**脫敏結果如下: **

image

執行流程分析:

我添加了輸出語句,來分析他的執行流程

public class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {
    private SensitiveStrategy strategy;

    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        //strategy.desensitizer() 返一個Function
        // Function.apply(value) 執行枚舉里面定義的脫敏方法
        gen.writeString(strategy.desensitizer().apply(value));
        System.out.println(4);
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
        System.out.println(1);
        Sensitive annotation = property.getAnnotation(Sensitive.class);
        if (Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass())) {
            this.strategy = annotation.strategy();
            System.out.println(2);
            return this;
        }
        System.out.println(3);
        return prov.findValueSerializer(property.getType(), property);
    }
}

執行后打印數據如下:

image

說明在進行序列化的時候,框架先掃描到了實體類的該注解 @Sensitive(strategy = SensitiveStrategy.ID_CARD)

然后根據該注解里面的 @JsonSerialize(using = SensitiveJsonSerializer.class) 使用了我們自定義的序列化器

先執行了createContextual方法,來獲取上下文(獲取注解里面的參數 SensitiveStrategy.ID_CARD)

然后執行序列化方法serialize,該方法會獲取前面的createContextual方法返回的參數 (這里就是 value)

strategy.desensitizer() 返回的是一個函數

.apply(value) 使用的是jdk8 的Function.apply() 會執行strategy.desensitizer()返回的函數

gen.writeString(strategy.desensitizer().apply(value)) 然后把函數的返回值設置給序列化的對象

結語 :

(1)整個的執行流程如上所示,需要更加深刻了解的可以在代碼里面進行debug,跟蹤他執行的每一步,進行理解

(2)該方法時基於springboot默認的Jackson進行的,如果序列化框架是fastjson的話,需要進行修改(待補充)

參考鏈接 :

https://mp.weixin.qq.com/s/GmELzTYIwYAIpTVRyCh9mw


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM