利用Jackson序列化实现数据脱敏

Posted Jcloud

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了利用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])\\\\d8");




    @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序列化实现数据脱敏的全过程,如有此类需求的同学可以借鉴上面的实现方法。

如何优雅的实现数据脱敏

如何优雅的实现数据脱敏


很多时候我们从ORM查询到的数据有其它逻辑要处理,比如根据电话号查询用户信息,你脱敏了就没有办法来处理该逻辑了。所以脱敏这个步骤需要后置,放在JSON序列化这个阶段比较合适。今天就来实现这个功能。

Jackson序列化中脱敏

改造过程其实就是把脱敏后置到JSON序列化过程中,这里我使用Jackson类库。

原来Mybatis插件中的脱敏注解是这样的:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Sensitive 
    SensitiveStrategy strategy();

脱敏的策略是这样的:

import java.util.function.Function;

/**
 * 脱敏策略.
 *
 * @author felord.cn
 * @since 11 :25
 */
public enum SensitiveStrategy 
    /**
     * Username sensitive strategy.
     */
    USERNAME(s -> s.replaceAll("(\\\\S)\\\\S(\\\\S*)", "$1*$2")),
    /**
     * Id card sensitive type.
     */
    ID_CARD(s -> s.replaceAll("(\\\\d4)\\\\d10(\\\\w4)", "$1****$2")),
    /**
     * Phone sensitive type.
     */
    PHONE(s -> s.replaceAll("(\\\\d3)\\\\d4(\\\\d4)", "$1****$2")),
    /**
     * Address sensitive type.
     */
    ADDRESS(s -> s.replaceAll("(\\\\S3)\\\\S2(\\\\S*)\\\\S2", "$1****$2****"));


    private final Function<String, String> desensitizer;

    SensitiveStrategy(Function<String, String> desensitizer) 
        this.desensitizer = desensitizer;
    

    public Function<String, String> desensitizer() 
        return desensitizer;
    

我将改造它们,让它们在JSON序列化中实现字段属性脱敏。

自定义脱敏序列化

这里我们首先实现自定义的脱敏序列化逻辑:

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;

import java.io.IOException;
import java.util.Objects;

/**
 * @author felord.cn
 * @since 1.0.8.RELEASE
 */
public class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer 
    private SensitiveStrategy strategy;

    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException 
           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);

    

其中createContextual方法用来获取实体类上的@Sensitive注解并根据条件初始化对应的JsonSerializer对象;而顾名思义,serialize方法执行脱敏序列化逻辑。

改造脱敏注解

然后就是改造@Sensitive,把上面自定义的JSON序列化和脱敏策略绑定到一起。这里用到了Jackson的捆绑注解@JacksonAnnotationsInside,它的作用是将多个注解组合到一起;另外一个是序列化注解@JsonSerialize,它的作用是声明使用我上面自定义的序列化方法。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveJsonSerializer.class)
public @interface Sensitive 
    SensitiveStrategy strategy();

使用

这就改造完成了,我们来试一试。我们定义一个需要脱敏的实体类并根据字段标记上对应的脱敏注解:

/**
 * @author felord.cn
 * @since 1.0.8.RELEASE
 */
@Data
public class User 

    /**
     * 真实姓名
     */
    @Sensitive(strategy = SensitiveStrategy.USERNAME)
    private String realName;
    /**
     * 地址
     */
    @Sensitive(strategy = SensitiveStrategy.ADDRESS)
    private String address;
    /**
     * 电话号码
     */
    @Sensitive(strategy = SensitiveStrategy.PHONE)
    private String phoneNumber;
    /**
     * 身份证号码
     */
    @Sensitive(strategy = SensitiveStrategy.ID_CARD)
    private String idCard;

然后Jackson来序列化User实例:

        User user = new User();

        user.setRealName("张三丰");
        user.setPhoneNumber("13333333333");
        user.setAddress("湖北省十堰市丹江口市武当山");
        user.setIdCard("4333333333334334333");

        ObjectMapper objectMapper = new ObjectMapper();

        String json = objectMapper.writeValueAsString(user);

        System.out.println(json);

可以得到:


    "realName":"张*丰",
    "address":"湖北省****市丹江口市武****",
    "phoneNumber":"133****3333",
    "idCard":"4333****34333"

效果还是可以的,当然如果能加个开关就更好了,根据不同的场景来决定是否进行脱敏。这个以后在研究研究,好了今天的分享就到这里


参考

我用注解优雅的实现了数据的脱敏

改造了以前写的数据脱敏插件,更好用了

Jackson 进阶之自定义序列化器

Jackson总结:常用注解、整合spring、自定义JsonSerializer

以上是关于利用Jackson序列化实现数据脱敏的主要内容,如果未能解决你的问题,请参考以下文章

如何优雅的实现数据脱敏

Jackson注解自定义数据脱敏策略

Jackson注解自定义数据脱敏策略

JAVA数据脱敏

java 日志的数据脱敏

3种常见的数据脱敏方案