spring boot实现接口数据脱敏,整合jackson实现敏感信息隐藏脱敏

文章目录

整合 jackson 实现接口数据脱敏

目的:整合 jackson 实现接口数据脱敏(对涉及到敏感信息 的字段进行部分隐藏或替换处理 ,以防止敏感数据泄露)

先了解部分代码涉及到的知识点,再来看完整代码实现:

1、 自定义序列化器的时候,需要 继承 JsonSerializer 类 和 实现 ContextualSerializer 接口,如下:

这里只贴出结构,代码的逻辑部分没有贴出(后面会讲解)

java 复制代码
public class MySensitiveSerialize extends JsonSerializer<String> implements ContextualSerializer {

    // 来自 JsonSerializer 重写
    @Override
    public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        
    }

    // 来自 ContextualSerializer 重写
    @Override
    public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
        return null;
    }
}
JsonSerializer类
  1. JsonSerializer<T> 是 Jackson 用来 控制 Java 对象序列化为 JSON 的核心接口。泛型 <T> 指的是这个序列化器能处理的数据类型。

    • JsonSerializer<String> → 这个序列化器只处理 String 类型字段。

    • JsonSerializer<Object> → 可以处理任何类型,适合通用序列化器。

  2. 当 Jackson 遇到一个字段需要序列化成 JSON 时:

    1. 找到对应字段类型的序列化器:默认会有 Jackson 提供的内置序列化器,比如:

      • StringSerializer 处理 String
      • IntegerSerializer 处理 Integer
    2. 如果字段上标注了 自定义序列化器(比如:@JsonSerialize(using = YourSerializer.class),就会用你提供的序列化器。

ContextualSerializer接口

ContextualSerializer 是 Jackson 提供的接口,允许序列化器在运行时动态生成 ,根据 字段上的注解或者上下文,生成一个"带配置"的序列化器实例。

这句话到底什么意思?----> 假设现在需要脱敏:

  • 手机号 → 138****5678
  • 邮箱 → u***@example.com
  • 姓名 → 张*丰

希望同一个 序列化器 能处理不同字段的不同脱敏规则。

  • 如果不使用 ContextualSerializer
    • 每个字段都要写一个单独的序列化器类(比如:PhoneSerializerEmailSerializerNameSerializer)。不灵活,代码冗余。
  • 使用 ContextualSerializer
    • Jackson 在序列化字段时,会调用 createContextual方法 (后面会详细讲解这个方法)
    • 可以读取字段上的注解(比如 @SensitiveWrapped(SensitiveEnum.MOBILE)
    • 根据注解动态生成序列化器实例,实现 同一个类,不同字段,不同脱敏逻辑

举例:

java 复制代码
public class User {
    @SensitiveWrapped(SensitiveEnum.MOBILE)
    private String mobile;

    @SensitiveWrapped(SensitiveEnum.EMAIL)
    private String email;
}

序列化流程:@SensitiveWrapped、SensitiveEnum均为自定义的类(在后续整个代码实现中会写出,这里只是为了看知识点)

  1. Jackson 扫描 User 的字段
  2. 遇到 mobile 字段,调用 SensitiveSerialize.createContextual(serializerProvider, property)
    • 读取字段上的注解 @SensitiveWrapped(SensitiveEnum.MOBILE)
    • 返回一个 带 SensitiveEnum.MOBILE 配置的序列化器实例
  3. 调用 serialize 方法:
    • 传入 mobile 字段的值
    • 根据 SensitiveEnum.MOBILE 执行脱敏 → "138****5678"
  4. 遇到 email 字段,同样流程,但注解是 SensitiveEnum.EMAIL
    • 返回一个带 SensitiveEnum.EMAIL 配置的序列化器实例
    • 输出 "u***@example.com"

2、 自定义注解,如下:

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveSerialize.class)
public @interface SensitiveWrapped {
    // 使用注解时,给注解中添加值
    SensitiveEnum value();
}

1.1 没有 @Target 注解,默认可以应用到所有 Java 元素(相当于没有限制)

1.2 @JacksonAnnotationsInside 注解

这是 Jackson 提供的一个元注解。它的效果是:把当前注解视作"组合注解" 。即当某个字段上标注了 @SensitiveWrapped,Jackson 会把 @SensitiveWrapped 当作内部声明的 Jackson 注解的代理。

正常情况下,Jackson 只认它自己的注解,如果没有 @JacksonAnnotationsInside,Jackson 不会自动识别出这个注解。

这里就是:@SensitiveWrapped 内部又标注了 @JsonSerialize,所以 Jackson 会像直接在字段上写 @JsonSerialize(using = SensitiveSerialize.class) 那样处理。

好处:把 Jackson 的配置封装到自定义注解中,使用方只要写 @SensitiveWrapped(SensitiveEnum.MOBILE) 就可以了,语义更清晰、代码更简洁。

createContextual方法

3、 详细分析 ContextualSerializer 接口中实现的 createContextual 方法:

java 复制代码
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
    // 为空直接跳过
    if (beanProperty != null) {
        // 非String类直接跳过:因为这里验证的类型都是String
        if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
            // 拿到注解
            SensitiveWrapped annotation = beanProperty.getAnnotation(SensitiveWrapped.class);
            if (annotation == null) {
                annotation = beanProperty.getContextAnnotation(SensitiveWrapped.class);
            }
            if (annotation != null) {
                // 如果能得到注解,就将注解的value传入序列化器中
                return new MySensitiveSerialize(annotation.value());
            }
        }
        return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
    }
    return serializerProvider.findNullValueSerializer(beanProperty);
}

① 代码目的:在序列化某个字段之前,根据 字段的类型 与 注解 决定返回哪一个 JsonSerializer

  • 若字段是 String 且标注了 @SensitiveWrapped 注解,就返回一个"带脱敏配置"的自定义序列化器实例;
  • 否则返回 Jackson 默认的序列化器。

方法的核心是把注解信息(annotation.value())传入序列化器,从而实现"同一个序列化器类,不同字段不同脱敏规则"的能力(ContextualSerializer 的典型用法)

createContextual方法签名分析:

  • 参数分析:

    • SerializerProvider serializerProvider:Jackson 的序列化提供者,能用于查找默认序列化器或做其他上下文相关操作。

    • BeanProperty beanProperty:表示当前正在序列化的 bean 的属性信息(字段/方法/构造参数等)。从 beanProperty 可以读取属性类型、注解、名称等元数据。(后面会详细分析)

  • 返回值分析:一个 JsonSerializer<?> 序列化实例:这个序列化器会给 serialize(...) 调用。返回值可以是 this、一个新的序列化器实例,或 Jackson 的默认序列化器。

③ 代码分析:

3.1 beanProperty.getType().getRawClass()beanProperty.getType() 返回 JavaTypegetRawClass() 返回原始 Class

3.2 读取注解:(包含两步)

java 复制代码
SensitiveAnno annotation = beanProperty.getAnnotation(SensitiveWrapped.class);
if (annotation == null) {
    annotation = beanProperty.getContextAnnotation(SensitiveWrapped.class);
}

含义:尝试从 beanProperty 上读取 @SensitiveWrapped 注解,顺序为:

  1. beanProperty.getAnnotation(...):直接读取属性本身上的注解(即标在字段 或 getter/setter上的注解)。
  2. beanProperty.getContextAnnotation(...):如果前者没找到,再检查"上下文注解"(比如类级别、接口、方法上的注解等视具体实现而定)。

为什么检查 2 次注解:

  • 注解可能直接标在字段或 getter(getAnnotation 能拿到)。但有时注解放在类级别或更高级的上下文位置(或由于代理/继承关系),getContextAnnotation 能发现某些在上下文中可见的注解。确保在不同标注位置也能生效。

**3.3 ** 如果找到注解,返回带配置的序列化器:

java 复制代码
if (annotation != null) {
    // 如果能得到注解,就将注解的value传入序列化器中
    return new MySensitiveSerialize(annotation.value());
}

如果没找到注解或不是 String,则交给 Jackson 的默认序列化器去处理(即按照普通规则序列化该字段)

java 复制代码
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);

findValueSerializer 的作用:根据字段类型和属性元信息,返回 Jackson 已注册或内建的序列化器(例如 StringSerializerIntegerSerializer 等)

3.4beanProperty == null 时:表示当前上下文不是某个 bean 的具体属性。

java 复制代码
// 不应该这么写:应该返回默认的序列化器
return serializerProvider.findNullValueSerializer(beanProperty);

这里不应该这么写,应该返回默认的序列化器:

java 复制代码
return serializerProvider.findValueSerializer(String.classs, beanProperty(或者直接写null,因为beanProperty值就是null));

问题1:为什么一定是 String.class,原来的类型不可能是别的类型吗?----> 因为自定义的序列化器(当前这个序列化器)是针对 String 类型的,所以我就返回原先的默认序列化即可:

java 复制代码
public class MySensitiveSerialize extends JsonSerializer<String> implements ContextualSerializer {
    ......
}

或者更明确一点,让 Jackson 自动根据泛型推导出,改为:

java 复制代码
return serializerProvider.findValueSerializer(this.handledType(), beanProperty);

问题2:可以直接返回 this 不?

直接返回 this,Jackson 会继续用你当前这个序列化器。比如:

java 复制代码
/* 假如要序列化的值是 "123456" 
然后 beanProperty = null(因为是根元素)
*/
// 假设你返回this:
return this;
// 则继续当前的序列化器
// 那还是我们自定义的序列化器
// 结果:就会将123456脱敏

// 假如你写
return serializerProvider.findValueSerializer(this.handledType(), beanProperty);
// 那么 Jackson 会还原为系统自带的 StringSerializer,不会脱敏

3.4.1 再次分析下 BeanProperty BeanProperty 表示 当前被序列化的属性(字段)

1、如果 Jackson 正在序列化一个 Java Bean(例如 User 对象)的字段 name,那么 beanProperty 就会包含这个字段的元信息。

例如:

java 复制代码
public class User {
    private String name;
}

当 Jackson 序列化 user.name 时:beanProperty ≠ null。它包含的信息包括:

  • 字段名(name
  • 字段类型(String
  • 该字段上的注解(比如你的 @SensitiveWrapped
  • 所属类(User.class

2、什么时候 beanProperty == null?----> 当 Jackson 在序列化的值不是某个类的"字段",而是一个独立的值(根对象(直接字符串或对象)、集合元素、Map 值、null值等),就拿不到 Bean 属性信息,因此 beanProperty == null

例如:

java 复制代码
// 根对象String:也就是直接定义字符串
String = "ok";
// List<String>、Map<String, String> 
List<String> list = Arrays.asList("java","C"); 

相当于 Jackson 只知道这是一个什么,但具体不知道这个东西是属于哪个类的哪个字段。但假如集合中是某个具体的对象(比如:List<User>),则就知道了是哪个 bean,此时就不为 null


总结:根据上面的分析给出更清晰的版本

java 复制代码
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
// 如果beanProperty为null
if (beanProperty == null) {
    // 返回默认的字符串序列化器
    return serializerProvider.findValueSerializer(this.handledType(), null);
}

// 只对String类型处理
JavaType type = beanProperty.getType();
if (!Object.equals(type.getRawClass(), String.class)) {
    // 非String类型使用默认的序列化器
    return serializerProvider.findValueSerializer(type, beanProperty);
}

// 尝试读取注解:先读属性本身,再读上下文注解
SensitiveAnno ann = property.getAnnotation(SensitiveWrapped.class);
if (ann == null) {
    ann = property.getContextAnnotation(SensitiveWrapped.class);
}

if (ann != null) {
    // 找到注解 -> 返回带具体脱敏配置的序列化器实例(应为不可变、线程安全)
    return new MySensitiveSerialize(ann.value());
}

// 没找到注解 -> 返回默认的 String 序列化器
return serializerProvider.findValueSerializer(type, beanProperty);
完整代码实现

可以参考上传到 Gitee 中的代码:完整代码

运行结果:

结束,不要忘记关注收藏哦,不懂请评论!

相关推荐
小猪咪piggy2 分钟前
【项目】小型支付商城 MVC/DDD
java·jvm·数据库
知兀6 分钟前
【Spring/SpringBoot】SSM(Spring+Spring MVC+Mybatis)方案、各部分职责、与Springboot关系
java·spring boot·spring
向葭奔赴♡7 分钟前
Spring IOC/DI 与 MVC 从入门到实战
java·开发语言
早退的程序员8 分钟前
记一次 Maven 3.8.3 无法下载 HTTP 仓库依赖的排查历程
java·http·maven
向阳而生,一路生花11 分钟前
redis离线安装
java·数据库·redis
Tigshop开源商城系统11 分钟前
Tigshop 开源商城系统 php v5.1.9.1版本正式发布
java·大数据·开源·php·开源软件
2401_8414956428 分钟前
【数据结构】基于BF算法的树种病毒检测
java·数据结构·c++·python·算法·字符串·模式匹配
little_xianzhong29 分钟前
三个常听到的消息/中间件MQTT RabbitMQ Kafka
java·笔记·中间件·消息队列
论迹37 分钟前
【Spring Cloud 微服务】-- 服务拆分原则
java·spring cloud·微服务
汤姆yu40 分钟前
基于springboot的民间救援队救助系统
java·spring boot·后端·救援队