Jackson序列化方式实现数据字段脱敏(工厂模式 + 策略模式)

424 阅读4分钟

Jackson序列化方式实现数据脱敏


方案选择

  • 基于Mybatis 的拦截器:对select 语句进行拦截数据脱敏,但是存在问题 在某些业务中对数据脱敏字段是需要进行逻辑业务处理的
  • 基于Jaskson 序列化:针对需要展示在前端的数据 通常我们都是对应一个VO对象,这里可以在将VO对象返回给前端序列化时进行数据的脱敏

实现方案

1.自定义序列化器 覆盖原来的序列化方式

 ​
     需要引入jackson依赖
      <dependency>
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-annotations</artifactId>
             <version>2.11.0</version>
         </dependency>
         <dependency>
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-databind</artifactId>
             <version>2.11.0</version>
         </dependency>
 ​
 ​
 /**
  * 脱敏序列化器
  */
 public class ObjectDesensitizeSerializer extends StdSerializer<Object> implements ContextualSerializer {
 ​
    
     protected ObjectDesensitizeSerializer() {
         super(Object.class);
     }
 ​
     @Override
     public JsonSerializer<Object> createContextual(SerializerProvider prov, BeanProperty property) {
         
         //  这里创建序列化上下文环境 以选择是否需要指定返回不同的序列化器
         
         
         return serializer;
     }
 ​
     @Override
     public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        
         //  这里编写实际序列化的字段的处理
         
     }
 ​
 }

2.定义策略类(处理实际脱敏逻辑)

然后针对不同的字段会有不同的脱敏方案 比如 身份证号 银行卡号 电话号码等

这里采用 策略模式 + 工厂模式 进行解耦,符合开闭原则

定义一个顶级接口 然后多个实现

 /**
  * 定义一个 顶级的脱敏器
  */
 public interface Desensitization<T> {
 ​
 ​
     /**
      * 脱敏实现
      *
      * @param target 脱敏对象
      * @return 脱敏返回结果
      */
     T desensitize(T target);
 ​
 }
 ​
 //  多接口实现
 ​
 /**
  * 字符串脱敏器
  */
 public interface StringDesensitization extends Desensitization<String> {
 }
 ​
 /**
  * 身份证(18位和15位) 脱敏器
  */
 public class IDCardDesensitization implements StringDesensitization {
 }
 ​
 ​
 /**
  * 邮箱脱敏器 默认只保留域名
  */
 public class EmailDesensitization implements StringDesensitization {
 }
 ​

这里只展示具体的一个脱敏实现 其他都是类似

 /**
  * 手机号脱敏器 默认只保留前3位和后4位
  */
 public class PhoneDesensitization implements StringDesensitization {
     /**
      * 手机号正则
      */
     private static final Pattern DEFAULT_PATTERN = Pattern.compile("(13[0-9]|14[579]|15[0-3,5-9]|16[6]|17[0135678]|18[0-9]|19[89])\d{8}");
     /**
      * 手机号脱敏 只保留前3位和后4位
      */
     @Override
     public String desensitize(String target) {
         //  匹配判断是否符合正则
         Matcher matcher = DEFAULT_PATTERN.matcher(target);
         while (matcher.find()) {
             String group = matcher.group();
             
             //  调用方法进行脱敏
             target = target.replace(group, group.substring(0, 3) + Symbol.getSymbol(4, Symbol.STAR) + group.substring(7, 11));
         }
         return target;
     }
 }
 ​
 public class Symbol {
     /**
      * '*'脱敏符
      */
     public static final String STAR = "*";
 ​
     private Symbol() {
 ​
     }
 ​
     /**
      * 获取符号
      *
      * @param number 符号个数
      * @param symbol 符号
      */
     public static String getSymbol(int number, String symbol) {
         StringBuilder sb = new StringBuilder();
         for (int i = 0; i < number; i++) {
             sb.append(symbol);
         }
         return sb.toString();
     }
 }

3.定义工厂类 获取实际策略 并缓存策略

上述是 策略模式的体现 ,策略模式 通常结合工厂模式 屏蔽创建对象细节 直接通过工厂创建指定的策略类

 /**
  *策略类的工厂
  */
 public class DesensitizationFactory {
 ​
     private DesensitizationFactory() {
     }
 ​
     //  这里采用一个 Map 集合 对指定的策略类进行缓存 避免对象的重复创建
     private static final Map<Class<?>, Desensitization<?>> map = new HashMap<>();
 ​
     
     @SuppressWarnings("all")
     public static Desensitization<?> getDesensitization(Class<?> clazz) {
         //  如果传递的只是接口 不是实现类 则抛出异常
         if (clazz.isInterface()) {
             throw new UnsupportedOperationException("desensitization is interface, what is expected is an implementation class !");
         }
         return map.computeIfAbsent(clazz, k -> {
             try {
                 //  返回指定 Class 的策略类 同时缓存在 Map 中
                 return (Desensitization<?>) clazz.newInstance();
             } catch (InstantiationException | IllegalAccessException e) {
                 throw new UnsupportedOperationException(e.getMessage(), e);
             }
         });
     }
 ​
 }

4.定义注解 简化使用

然后就是注解的定义,利用注解 直接对 VO 对象的字段标记,无代码入侵

 /**
  * 对象脱敏 注解
  */
 @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) // 作用于注解类型上 供其他注解使用
 @Retention(RetentionPolicy.RUNTIME)
 @JacksonAnnotationsInside  //  组合注解 将多个注解拼在一起使用
 @JsonSerialize(using = ObjectDesensitizeSerializer.class) //  标记序列化配置中 使用哪个类序列化 这里指定之前定义的ObjectDesensitizeSerializer
 @Documented
 public @interface Desensitize {
     
     //  这里对应了工厂类中的Class类型 以及在 ObjectDesensitizeSerializer 中创建上下文环境时 可供获取的参数去选择实际的脱敏方式
     
     /**
      * 对象脱敏器实现
      */
     @SuppressWarnings("all")
     Class<? extends Desensitization<?>> desensitization();
 ​
 ​
 }

然后就是对应的实际字段注解

 /**
  * 电话脱敏 注解
  */
 @Target({ElementType.FIELD})
 @Retention(RetentionPolicy.RUNTIME)
 @JacksonAnnotationsInside
 @Desensitize(desensitization = PhoneDesensitization.class)  //  这里指定执行脱敏逻辑的类
 @Documented
 public @interface PhoneDesensitize {
 }
 ​
 /**
  * 中华人民共和国身份证 脱敏注解
  */
 @Target({ElementType.FIELD})
 @Retention(RetentionPolicy.RUNTIME)
 @JacksonAnnotationsInside
 @Desensitize(desensitization = IDCardDesensitization.class) //  这里指定执行脱敏逻辑的类
 @Documented
 public @interface IDCardDesensitize {
 }

5.完善自定义序列化器的逻辑

最后就是回到开头 编写实际的 自定义序列化器

 **
  * 脱敏序列化器
  */
 public class ObjectDesensitizeSerializer extends StdSerializer<Object> implements ContextualSerializer {
     
     
     //  面向接口 根据策略不同而实际 set 不同的 脱敏类
     private transient Desensitization<Object> desensitization;
 ​
     protected ObjectDesensitizeSerializer() {
         super(Object.class);
     }
 ​
     public Desensitization<Object> getDesensitization() {
         return desensitization;
     }
 ​
     public void setDesensitization(Desensitization<Object> desensitization) {
         this.desensitization = desensitization;
     }
 ​
     //  创建上下文环境
     @Override
     public JsonSerializer<Object> createContextual(SerializerProvider prov, BeanProperty property) {
         //  根据 BeanProperty 获取被标记 VO 字段的注解上的 实际策略脱敏类
         Desensitize annotation = property.getAnnotation(Desensitize.class);
         return createContextual(annotation.desensitization());
     }
 ​
     @SuppressWarnings("unchecked")
     public JsonSerializer<Object> createContextual(Class<? extends Desensitization<?>> clazz) {
         ObjectDesensitizeSerializer serializer = new ObjectDesensitizeSerializer();
         
         //  判断是否属于 StringDesensitization 因为 StringDesensitization 属于全脱敏
         if (clazz != StringDesensitization.class) {
             
             //  不属于则 回到上面 通过工厂类去创建脱敏类
             serializer.setDesensitization((Desensitization<Object>) DesensitizationFactory.getDesensitization(clazz));
         }
         return serializer;
     }
 ​
     
     //  创建完 上下文环境 返回 serializer 执行序列化 serialize
     @Override
     public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
         Desensitization<Object> objectDesensitization = getDesensitization();
         
         //  获取 策略类(即 前面 set 的 desensitization)  
         
         
         if (objectDesensitization != null) {
             try {
                 //  不为空调用处理脱敏逻辑
                 gen.writeObject(objectDesensitization.desensitize(value));
             } catch (Exception e) {
                 gen.writeObject(value);
             }
         } else if (value instanceof String) {
             //  为空 说明是 StringDesensitization  且字段是 String 类型 则 全脱敏
             gen.writeString(Symbol.getSymbol(((String) value).length(), Symbol.STAR));
         } else {
             //  否则 正常序列化
             gen.writeObject(value);
         }
     }
 }

最终可达到 一个注解 即可脱敏

image-20220817172318562