自定义注解实现数据序列化时进行数据脱敏
在最近的开发工作中遇到了需要对身份证号码进行脱敏的操作, 开始的想法特别简单,就是在数据返回的时候进行数据的脱敏操作,示例代码如下:
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的话,需要进行修改(待补充)
参考链接 :