乐趣区

关于数据结构:利用Jackson序列化实现数据脱敏

作者:京东物流 张晓旭

1. 背景

在我的项目中有些敏感信息不能间接展现,比方客户手机号、身份证、车牌号等信息,展现时均须要进行数据脱敏,避免泄露客户隐衷。脱敏即是对数据的局部信息用脱敏符号(*)解决。

2. 指标

  • 在服务端返回数据时,利用 Jackson 序列化实现数据脱敏,达到对敏感信息脱敏展现。
  • 升高反复开发量,晋升开发效率
  • 造成对立无效的脱敏规定
  • 可基于重写默认脱敏实现的 desensitize 办法,实现可扩大、可自定义的个性化业务场景的脱敏需要

3. 次要实现

3.1 基于 Jackson 的自定义脱敏序列化实现

StdSerializer:所有规范序列化程序所应用的基类,这个是编写自定义序列化程序所举荐应用的基类。
ContextualSerializer: 是 Jackson 提供的另一个序列化相干的接口,它的作用是通过字段已知的上下文信息定制 JsonSerializer。

package com.jd.ccmp.ctm.constraints.serializer;




import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.jd.ccmp.ctm.constraints.Symbol;
import com.jd.ccmp.ctm.constraints.annotation.Desensitize;
import com.jd.ccmp.ctm.constraints.desensitization.Desensitization;
import com.jd.ccmp.ctm.constraints.desensitization.DesensitizationFactory;
import com.jd.ccmp.ctm.constraints.desensitization.DefaultDesensitization;




import java.io.IOException;




/**
 * 脱敏序列化器
 *
 * @author zhangxiaoxu15
 * @date 2022/2/8 11:10
 */
public class ObjectDesensitizeSerializer extends StdSerializer<Object> implements ContextualSerializer {
    private static final long serialVersionUID = -7868746622368564541L;
    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) {
// 获取属性注解
        Desensitize annotation = property.getAnnotation(Desensitize.class);
        return createContextual(annotation.desensitization());
    }
    @SuppressWarnings("unchecked")
    public JsonSerializer<Object> createContextual(Class<? extends Desensitization<?>> clazz) {ObjectDesensitizeSerializer serializer = new ObjectDesensitizeSerializer();
        if (clazz != DefaultDesensitization.class) {serializer.setDesensitization((Desensitization<Object>) DesensitizationFactory.getDesensitization(clazz));
        }
        return serializer;
    }
    @Override
    public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {Desensitization<Object> objectDesensitization = getDesensitization();
        if (objectDesensitization != null) {
            try {gen.writeObject(objectDesensitization.desensitize(value));
            } catch (Exception e) {gen.writeObject(value);
            }
        } else if (value instanceof String) {gen.writeString(Symbol.getSymbol(((String) value).length(), Symbol.STAR));
        } else {gen.writeObject(value);
        }

注:createContextual 能够取得字段的类型以及注解。当字段领有自定义注解时,取出注解中的值创立定制的序列化形式,这样在 serialize 办法中便能够失去这个值了。createContextual 办法只会在第一次序列化字段时调用(因为字段的上下文信息在运行期不会扭转),所以无需关怀性能问题。

3.2 定义脱敏接口、以及工厂实现

3.2.1 脱敏器接口定义

package com.jd.ccmp.ctm.constraints.desensitization;


/**
 * 脱敏器
 *
 * @author zhangxiaoxu15
 * @date 2022/2/8 10:56
 */
public interface Desensitization<T> {
    /**
     * 脱敏实现
     *
     * @param target 脱敏对象
     * @return 脱敏返回后果
     */
    T desensitize(T target);
}

3.2.2 脱敏器工厂实现

package com.jd.ccmp.ctm.constraints.desensitization;


import java.util.HashMap;
import java.util.Map;


/**
 * 工厂办法
 *
 * @author zhangxiaoxu15
 * @date 2022/2/8 10:58
 */
public class DesensitizationFactory {private DesensitizationFactory() { }
    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, key -> {
            try {return (Desensitization<?>) clazz.newInstance();} catch (InstantiationException | IllegalAccessException e) {throw new UnsupportedOperationException(e.getMessage(), e);
            }
        });

3.3 罕用的脱敏器实现

3.3.1 默认脱敏实现

可基于默认实现,扩大实现个性化场景

package com.jd.ccmp.ctm.constraints.desensitization;


/**
 * 默认脱敏实现
 *
 * @author zhangxiaoxu15
 * @date 2022/2/8 11:01
 */
public interface DefaultDesensitization extends Desensitization<String> {}

3.3.2 手机号脱敏器

实现对手机号两头 4 位号码脱敏

package com.jd.ccmp.ctm.constraints.desensitization;
import com.jd.ccmp.ctm.constraints.Symbol;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
 * 手机号脱敏器,保留前 3 位和后 4 位
 *
 * @author zhangxiaoxu15
 * @date 2022/2/8 11:02
 */
public class MobileNoDesensitization implements DefaultDesensitization {
    /**
     * 手机号正则
     */
    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}");




    @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;

3.4 注解定义

通过 @JacksonAnnotationsInside 实现自定义注解,进步易用性

package com.jd.ccmp.ctm.constraints.annotation;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.jd.ccmp.ctm.constraints.desensitization.Desensitization;
import com.jd.ccmp.ctm.constraints.serializer.ObjectDesensitizeSerializer;
import java.lang.annotation.*;


/**
 * 脱敏注解
 *
 * @author zhangxiaoxu15
 * @date 2022/2/8 11:09
 */
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = ObjectDesensitizeSerializer.class)
@Documented
public @interface Desensitize {
    /**
     * 对象脱敏器实现
     */
    @SuppressWarnings("all")
    Class<? extends Desensitization<?>> desensitization();

3.4.1 默认脱敏注解

package com.jd.ccmp.ctm.constraints.annotation;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.jd.ccmp.ctm.constraints.desensitization.DefaultDesensitization;
import java.lang.annotation.*;




/**
 * 默认脱敏注解
 *
 * @author zhangxiaoxu15
 * @date 2022/2/8 11:14
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@Desensitize(desensitization = DefaultDesensitization.class)
@Documented
public @interface DefaultDesensitize {

3.4.2 手机号脱敏注解

package com.jd.ccmp.ctm.constraints.annotation;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.jd.ccmp.ctm.constraints.desensitization.MobileNoDesensitization;
import java.lang.annotation.*;


/**
 * 手机号脱敏
 *
 * @author zhangxiaoxu15
 * @date 2022/2/8 11:18
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@Desensitize(desensitization = MobileNoDesensitization.class)
@Documented
public @interface MobileNoDesensitize {}

3.5 定义脱敏符号

反对指定脱敏符号,例如 * 或是 ^_^

package com.jd.ccmp.ctm.constraints;
import java.util.stream.Collectors;
import java.util.stream.IntStream;


/**
 * 脱敏符号
 *
 * @author zhangxiaoxu15
 * @date 2022/2/8 10:53
 */
public class Symbol {
    /**
     * '*' 脱敏符
     */
    public static final String STAR = "*";
    private Symbol() {}
    /**
     * 获取符号
     *
     * @param number 符号个数
     * @param symbol 符号
     */
    public static String getSymbol(int number, String symbol) {return IntStream.range(0, number).mapToObj(i -> symbol).collect(Collectors.joining());
    }

4. 应用样例 & 执行流程分析

程序类图

** 执行流程分析 **
 1. 调用 JsonUtil.toJsonString() 开始执行序列化
 2. 辨认属性 mobile 上的注解 @MobileNoDesensitize(上文 3.4.2)
 3. 调用 ObjectDesensitizeSerializer#createContextual(上文 3.1 & 3.2),返回 JsonSerializer
 4. 调用手机号脱敏实现 MobileNoDesensitization#desensitize(上文 3.3.2)
 5. 输入脱敏后的序列化后果,{"mobile":"133****5678"}

不难发现外围执行流程是第 3 步,然而 @MobileNoDesensitize 与 ObjectDesensitizeSerializer 又是如何分割起来的呢?

  • 尝试梳理下援用链路:@MobileNoDesensitize -> @Desensitize -> @JsonSerialize -> ObjectDesensitizeSerializer
  • 然而,在 ObjectDesensitizeSerializer 的实现中,咱们仿佛却没有发现上述链路的间接调用关系
  • 这就不得不说下 Jackson 元注解的概念
**Jackson 元注解 **
1. 提到元注解这个词,大家会想到 @Target、@Retention、@Documented、@Inherited
2.Jackson 也以同样的思路设计了 @JacksonAnnotationsInside


/**
 * Meta-annotation (annotations used on other annotations)
 * used for indicating that instead of using target annotation
 * (annotation annotated with this annotation),
 * Jackson should use meta-annotations it has.
 * This can be useful in creating "combo-annotations" by having
 * a container annotation, which needs to be annotated with this
 * annotation as well as all annotations it 'contains'.
 * 
 * @since 2.0
 */
@Target({ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotation
public @interface JacksonAnnotationsInside
{}

正是通过”combo-annotations”(组合注解、捆绑注解) 的机制,实现了批示 Jackson 应该应用其领有的元正文,而不是应用指标正文,从而实现了自定义脱敏实现设计指标。

5. 总结

以上就是利用 Jackson 序列化实现数据脱敏的全过程,如有此类需要的同学能够借鉴下面的实现办法。

退出移动版