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 中的代码:完整代码

运行结果:

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

相关推荐
咩咩啃树皮8 小时前
第40篇:Vue3组件化开发精讲——组件拆分、复用、父子通信、工程化架构
java·前端·架构
鱟鲥鳚8 小时前
Spring Boot 集成 LangChain4j:从模型调用到 Tool Calling(Demo版)
java·spring boot
大模型码小白10 小时前
【Python零基础教程】继承、多态与魔法函数:面向对象编程三大核心特性详解
java·大数据·开发语言·人工智能·python·ai编程
腾渊信息科技公司10 小时前
Spring Boot对接MES实战:视觉检测数据自动同步方案
java·人工智能·spring boot·后端·计算机视觉·ai·软件需求
爱笑的源码基地11 小时前
高并发 Redis 缓存门诊HIS系统源码,含财务统计药房进销存
java·程序·门诊系统·诊所系统·云诊所源码
wuqingshun31415912 小时前
TCP超时重传机制是为了解决什么问题?
java
莫逸风14 小时前
【AgentScope 2.0】 0. 学习指南
java·llm·agent·agentscope
隔窗听雨眠14 小时前
Spring Boot在云原生时代的编程范式革新研究
spring boot·后端·云原生
z1234567898615 小时前
2026最新两款AI编程工具深度对比实测
java·数据库·ai编程
yaoxin52112315 小时前
470. Java 反射 - Member 接口与 AccessFlag
java·开发语言·python