基于SpringBoot+Jackson实现注解式字段脱敏方案
在接口开发中,手机号、身份证、邮箱等敏感信息需要在返回前端时进行打码脱敏。本文基于 Jackson 序列化扩展机制,实现一套注解驱动的字段级精准脱敏方案,并使用 JDK17 新特性优化 switch 脱敏逻辑,代码更简洁、可读性更强,且全量代码可直接复制使用。
一、方案整体设计
1.1 核心思路
通过自定义 @Desensitize 注解标记需要脱敏的字段,结合 Jackson 的 AnnotationIntrospector(注解内省器)为标注字段动态绑定自定义序列化器,在 JSON 序列化阶段自动完成脱敏。整个过程无 AOP 代理、无反射性能损耗,仅对标注字段生效,完全不影响原生 Jackson 行为。
1.2 方案特点
- 字段级精准控制:仅标注
@Desensitize的字段执行脱敏,普通字段无任何影响 - JDK17 语法优化:使用 switch 表达式 + yield 实现脱敏分支,消除 break 冗余与穿透风险
- 类型安全:序列化器泛型限定为 String,非字符串字段自动跳过
- 边界完善:null 值、长度不合法、未知类型等场景均有兜底处理
- SpringBoot 无缝整合:通过
Jackson2ObjectMapperBuilderCustomizer自动注册
二、核心组件实现(完整可复制代码)
2.1 脱敏类型枚举
定义支持的脱敏场景,后续可按需扩展。
package com.example.desensitize.enums;
/**
* 脱敏类型枚举
*/
public enum DesensitizeType {
/** 手机号 */
PHONE,
/** 身份证号 */
ID_CARD,
/** 邮箱 */
EMAIL,
/** 姓名 */
NAME,
/** 地址 */
ADDRESS,
/** 自定义兜底 */
CUSTOM
}2.2 脱敏注解 @Desensitize
作用于字段上,指定脱敏类型;添加 @JacksonAnnotationsInside 让 Jackson 识别该自定义注解。
package com.example.desensitize.annotation;
import com.example.desensitize.enums.DesensitizeType;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 字段脱敏注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
public @interface Desensitize {
/**
* 脱敏类型
*/
DesensitizeType type();
}2.3 自定义序列化器(JDK17 Switch 优化重点)
继承 Jackson 的 JsonSerializer,在序列化方法中执行脱敏逻辑。核心优化点:使用 JDK17 switch 表达式替代传统 switch,箭头语法 + yield 返回值,代码更紧凑,分支逻辑更清晰。
package com.example.desensitize.serializer;
import com.example.desensitize.enums.DesensitizeType;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
/**
* 自定义脱敏序列化器
* 泛型限定为 String,仅字符串字段会触发
*/
public class DesensitizeSerializer extends JsonSerializer<String> {
private final DesensitizeType desensitizeType;
public DesensitizeSerializer(DesensitizeType desensitizeType) {
this.desensitizeType = desensitizeType;
}
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
// null 值直接输出 null,不进入脱敏逻辑
if (value == null) {
gen.writeNull();
return;
}
// 执行脱敏并写入结果
gen.writeString(doDesensitize(value, desensitizeType));
}
/**
* JDK17 switch 表达式实现脱敏逻辑
* 优势:无 break、无穿透风险,支持代码块内多逻辑,直接返回结果
*/
private String doDesensitize(String value, DesensitizeType type) {
return switch (type) {
case PHONE -> {
// 手机号:11位,保留前3后4,中间4位打码
if (value.length() != 11) yield value;
yield value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
case ID_CARD -> {
// 身份证:18位,保留前6后4,中间8位打码
if (value.length() != 18) yield value;
yield value.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");
}
case EMAIL -> {
// 邮箱:保留 @ 前首字符,其余打码
int atIndex = value.indexOf("@");
if (atIndex <= 1) yield value;
yield value.charAt(0) + "****" + value.substring(atIndex);
}
case NAME -> {
// 姓名:保留首字,其余字符打码
if (value.isEmpty()) yield value;
yield value.charAt(0) + "*".repeat(Math.max(0, value.length() - 1));
}
case ADDRESS -> {
// 地址:保留前6位,后续打码
if (value.length() <= 6) yield value;
yield value.substring(0, 6) + "****";
}
// 未知类型原样返回
default -> value;
};
}
}2.4 注解内省器
继承 JacksonAnnotationIntrospector,重写 findSerializer 方法,检测字段上的 @Desensitize 注解,动态返回对应脱敏序列化器。
package com.example.desensitize.introspector;
import com.example.desensitize.annotation.Desensitize;
import com.example.desensitize.serializer.DesensitizeSerializer;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
/**
* 脱敏注解内省器
* 负责将 @Desensitize 注解与自定义序列化器进行绑定
*/
public class DesensitizeAnnotationIntrospector extends JacksonAnnotationIntrospector {
@Override
public Object findSerializer(Annotated annotated) {
// 获取字段上的脱敏注解
Desensitize desensitize = annotated.getAnnotation(Desensitize.class);
// 仅当注解存在且字段类型为 String 时,返回自定义序列化器
if (desensitize != null && annotated.getRawType() == String.class) {
return new DesensitizeSerializer(desensitize.type());
}
// 无注解则走默认逻辑
return super.findSerializer(annotated);
}
}2.5 SpringBoot 自动配置
通过 Jackson2ObjectMapperBuilderCustomizer 将自定义模块注册到 Spring 容器的 ObjectMapper 中,内省器追加到链首保证优先级。
package com.example.desensitize.config;
import com.example.desensitize.introspector.DesensitizeAnnotationIntrospector;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Jackson 脱敏自动配置
*/
@Configuration
public class JacksonDesensitizeConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer desensitizeCustomizer() {
return builder -> {
// 1. 创建自定义模块
SimpleModule desensitizeModule = new SimpleModule("DesensitizeModule");
// 2. 注册注解内省器(序列化、反序列化均设置,脱敏仅在序列化阶段生效)
desensitizeModule.setAnnotationIntrospectors(
new DesensitizeAnnotationIntrospector(),
new DesensitizeAnnotationIntrospector()
);
// 3. 将模块注册到 ObjectMapper
builder.modules(desensitizeModule);
};
}
}三、完整执行流程解析
- 启动阶段:SpringBoot 启动时加载 Jackson 自动配置,执行我们的
desensitizeCustomizer,将自定义模块注册到全局ObjectMapper。 - 内省绑定:模块注册时,
DesensitizeAnnotationIntrospector被加入 Jackson 内省器链的首位,优先级高于默认内省器。 - 序列化触发:接口返回对象时,SpringMVC 调用
ObjectMapper将对象序列化为 JSON。 - 字段检测:遍历对象每个字段时,Jackson 依次调用内省器链的
findSerializer方法;我们的内省器优先检测字段是否标注@Desensitize且为 String 类型。 - 序列化执行:匹配成功则返回
DesensitizeSerializer实例,进入自定义serialize方法,通过 JDK17 switch 表达式执行对应脱敏规则,最终输出脱敏后的字符串。 - 兜底逻辑:无注解或非 String 字段,内省器返回 null,继续执行默认内省器逻辑,走原生 Jackson 序列化,完全不受影响。
四、设计原理与技术要点
4.1 为什么用注解内省器而不是全局类型序列化器
- 全局类型序列化器会对所有 String 字段生效,无法做到字段级精准控制;
- 注解内省器实现了「注解 + 字段」的细粒度绑定,只对目标字段生效,侵入性极低;
- 内省器追加到链首,不会覆盖
@JsonIgnore、@JsonProperty等原生 Jackson 注解功能。
4.2 JDK17 Switch 表达式的优势
- 语法简洁:箭头语法替代
case + break,消除样板代码; - 安全无穿透:每个分支独立,无需手动 break,彻底避免 switch 穿透 bug;
- 直接返回值:作为表达式直接赋值给变量,代码更紧凑;
- 代码块支持:复杂分支可使用
{}代码块,通过yield返回结果,兼顾简洁性与灵活性。
五、边界场景处理
| 场景 | 处理逻辑 |
|---|---|
| 字段值为 null | 直接输出 null,不进入脱敏方法 |
| 字段长度不符合规则(如手机号非11位) | 原样返回,不强制打码 |
| 字段非 String 类型 | 内省器类型校验不通过,走默认序列化 |
| 未标注 @Desensitize 注解 | 完全走原生 Jackson 逻辑,无任何影响 |
| 未知脱敏类型 | switch default 分支原样返回 |
六、调用案例与效果演示
6.1 实体类使用示例
在需要脱敏的字段上添加 @Desensitize 注解并指定类型。
package com.example.desensitize.vo;
import com.example.desensitize.annotation.Desensitize;
import com.example.desensitize.enums.DesensitizeType;
import lombok.Data;
@Data
public class UserVO {
private Long userId;
@Desensitize(type = DesensitizeType.NAME)
private String userName;
@Desensitize(type = DesensitizeType.PHONE)
private String phone;
@Desensitize(type = DesensitizeType.ID_CARD)
private String idCard;
@Desensitize(type = DesensitizeType.EMAIL)
private String email;
@Desensitize(type = DesensitizeType.ADDRESS)
private String address;
// 普通字段,不受脱敏影响
private Integer age;
}6.2 Controller 接口示例
package com.example.desensitize.controller;
import com.example.desensitize.vo.UserVO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/user")
public class UserController {
@GetMapping("/info")
public UserVO getUserInfo() {
UserVO user = new UserVO();
user.setUserId(1001L);
user.setUserName("张三丰");
user.setPhone("13812345678");
user.setIdCard("110101199001011234");
user.setEmail("zhangsan@example.com");
user.setAddress("北京市朝阳区建国路88号");
user.setAge(30);
return user;
}
}6.3 接口响应效果
{
"userId": 1001,
"userName": "张**",
"phone": "138****5678",
"idCard": "110101********1234",
"email": "z****@example.com",
"address": "北京市朝阳区****",
"age": 30
}七、方案总结
本方案基于 Jackson 原生扩展能力实现,无额外依赖、性能优异,配合 JDK17 的语法优化,代码可读性和可维护性大幅提升。所有组件开箱即用,只需将代码复制到项目中,在字段上添加注解即可完成脱敏,非常适合企业级项目的敏感信息处理场景。
以上就是基于SpringBoot+Jackson实现注解式字段脱敏方案的详细内容,更多关于SpringBoot Jackson注解式字段脱敏的资料请关注脚本之家其它相关文章!
相关文章
springboot配置过滤器和多个拦截器、执行顺序(案例详解)
这篇文章主要介绍了springboot配置过滤器和多个拦截器、执行顺序,在文章开头给大家介绍了过滤器配置的两种方法,创建两个拦截器,重写方法结合实例代码给大家介绍的非常详细,需要的朋友可以参考下2023-10-10


最新评论