1. 程式人生 > 其它 >自定義註解實現資料序列化時進行資料脫敏

自定義註解實現資料序列化時進行資料脫敏

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

  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;

**脫敏結果如下: **

執行流程分析:

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

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);
    }
}

執行後列印資料如下:

說明在進行序列化的時候,框架先掃描到了實體類的該註解 @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