spring boot实现接口数据脱敏,整合jackson实现敏感信息隐藏脱敏
文章目录
- 整合 jackson 实现接口数据脱敏
- JsonSerializer类
- ContextualSerializer接口
- createContextual方法
- 完整代码实现
整合 jackson 实现接口数据脱敏
目的:整合 jackson 实现接口数据脱敏(对涉及到敏感信息的字段进行部分隐藏或替换处理,以防止敏感数据泄露)
先了解部分代码涉及到的知识点,再来看完整代码实现:
1、 自定义序列化器的时候,需要 继承 JsonSerializer
类 和 实现 ContextualSerializer
接口,如下:
这里只贴出结构,代码的逻辑部分没有贴出(后面会讲解)
public class MySensitiveSerialize extends JsonSerializer<String> implements ContextualSerializer {// 来自 JsonSerializer 重写@Overridepublic void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {}// 来自 ContextualSerializer 重写@Overridepublic JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {return null;}
}
JsonSerializer类
-
JsonSerializer<T>
是 Jackson 用来 控制 Java 对象序列化为 JSON 的核心接口。泛型<T>
指的是这个序列化器能处理的数据类型。-
JsonSerializer<String>
→ 这个序列化器只处理String
类型字段。 -
JsonSerializer<Object>
→ 可以处理任何类型,适合通用序列化器。
-
-
当 Jackson 遇到一个字段需要序列化成 JSON 时:
-
找到对应字段类型的序列化器:默认会有 Jackson 提供的内置序列化器,比如:
StringSerializer
处理String
IntegerSerializer
处理Integer
-
如果字段上标注了 自定义序列化器(比如:
@JsonSerialize(using = YourSerializer.class)
,就会用你提供的序列化器。
-
ContextualSerializer接口
ContextualSerializer
是 Jackson 提供的接口,允许序列化器在运行时动态生成,根据 字段上的注解或者上下文,生成一个“带配置”的序列化器实例。
这句话到底什么意思?----> 假设现在需要脱敏:
- 手机号 →
138****5678
- 邮箱 →
u***@example.com
- 姓名 →
张*丰
希望同一个 序列化器 能处理不同字段的不同脱敏规则。
- 如果不使用
ContextualSerializer
:- 每个字段都要写一个单独的序列化器类(比如:
PhoneSerializer
、EmailSerializer
、NameSerializer
)。不灵活,代码冗余。
- 每个字段都要写一个单独的序列化器类(比如:
- 使用
ContextualSerializer
:- Jackson 在序列化字段时,会调用
createContextual
方法 (后面会详细讲解这个方法) - 可以读取字段上的注解(比如
@SensitiveWrapped(SensitiveEnum.MOBILE)
) - 根据注解动态生成序列化器实例,实现 同一个类,不同字段,不同脱敏逻辑
- Jackson 在序列化字段时,会调用
举例:
public class User {@SensitiveWrapped(SensitiveEnum.MOBILE)private String mobile;@SensitiveWrapped(SensitiveEnum.EMAIL)private String email;
}
序列化流程:@SensitiveWrapped、SensitiveEnum
均为自定义的类(在后续整个代码实现中会写出,这里只是为了看知识点)
- Jackson 扫描
User
的字段 - 遇到
mobile
字段,调用SensitiveSerialize.createContextual(serializerProvider, property)
:- 读取字段上的注解
@SensitiveWrapped(SensitiveEnum.MOBILE)
- 返回一个 带
SensitiveEnum.MOBILE
配置的序列化器实例
- 读取字段上的注解
- 调用
serialize
方法:- 传入
mobile
字段的值 - 根据
SensitiveEnum.MOBILE
执行脱敏 →"138****5678"
- 传入
- 遇到
email
字段,同样流程,但注解是SensitiveEnum.EMAIL
- 返回一个带
SensitiveEnum.EMAIL
配置的序列化器实例 - 输出
"u***@example.com"
- 返回一个带
2、 自定义注解,如下:
@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
方法:
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {// 为空直接跳过if (beanProperty != null) {// 非String类直接跳过:因为这里验证的类型都是Stringif (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()
返回 JavaType
,getRawClass()
返回原始 Class
。
3.2 读取注解:(包含两步)
SensitiveAnno annotation = beanProperty.getAnnotation(SensitiveWrapped.class);
if (annotation == null) {annotation = beanProperty.getContextAnnotation(SensitiveWrapped.class);
}
含义:尝试从 beanProperty
上读取 @SensitiveWrapped
注解,顺序为:
beanProperty.getAnnotation(...)
:直接读取属性本身上的注解(即标在字段 或 getter/setter上的注解)。beanProperty.getContextAnnotation(...)
:如果前者没找到,再检查“上下文注解”(比如类级别、接口、方法上的注解等视具体实现而定)。
为什么检查 2 次注解:
- 注解可能直接标在字段或 getter(
getAnnotation
能拿到)。但有时注解放在类级别或更高级的上下文位置(或由于代理/继承关系),getContextAnnotation
能发现某些在上下文中可见的注解。确保在不同标注位置也能生效。
**3.3 ** 如果找到注解,返回带配置的序列化器:
if (annotation != null) {// 如果能得到注解,就将注解的value传入序列化器中return new MySensitiveSerialize(annotation.value());
}
如果没找到注解或不是 String,则交给 Jackson 的默认序列化器去处理(即按照普通规则序列化该字段)
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
findValueSerializer
的作用:根据字段类型和属性元信息,返回 Jackson 已注册或内建的序列化器(例如 StringSerializer
、IntegerSerializer
等)
3.4 当 beanProperty == null
时:表示当前上下文不是某个 bean 的具体属性。
// 不应该这么写:应该返回默认的序列化器
return serializerProvider.findNullValueSerializer(beanProperty);
这里不应该这么写,应该返回默认的序列化器:
return serializerProvider.findValueSerializer(String.classs, beanProperty(或者直接写null,因为beanProperty值就是null));
问题1:为什么一定是 String.class
,原来的类型不可能是别的类型吗?----> 因为自定义的序列化器(当前这个序列化器)是针对 String 类型的,所以我就返回原先的默认序列化即可:
public class MySensitiveSerialize extends JsonSerializer<String> implements ContextualSerializer {......
}
或者更明确一点,让 Jackson 自动根据泛型推导出,改为:
return serializerProvider.findValueSerializer(this.handledType(), beanProperty);
问题2:可以直接返回 this 不?
直接返回 this,Jackson 会继续用你当前这个序列化器。比如:
/* 假如要序列化的值是 ”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
就会包含这个字段的元信息。
例如:
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
。
例如:
// 根对象String:也就是直接定义字符串
String = "ok";
// List<String>、Map<String, String>
List<String> list = Arrays.asList("java","C");
相当于 Jackson 只知道这是一个什么,但具体不知道这个东西是属于哪个类的哪个字段。但假如集合中是某个具体的对象(比如:List<User>
),则就知道了是哪个 bean,此时就不为 null
总结:根据上面的分析给出更清晰的版本
@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 中的代码:完整代码
运行结果:
结束,不要忘记关注收藏哦,不懂请评论!