自定义注解实现数据序列化时进行数据脱敏

在最近的开发工作中遇到了需要对身份证号码进行脱敏的操作, 开始的想法特别简单,就是在数据返回的时候进行数据的脱敏操作,示例代码如下:

  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

posted @ 2022-04-08 15:38  小猫爱哭鬼  阅读(1850)  评论(2编辑  收藏  举报