自定義註解實現資料序列化時進行資料脫敏
在最近的開發工作中遇到了需要對身份證號碼進行脫敏的操作, 開始的想法特別簡單,就是在資料返回的時候進行資料的脫敏操作,示例程式碼如下:
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的話,需要進行修改(待補充)
參考連結 :