基于SpringBoot+Jackson实现注解式字段脱敏方案

 更新时间:2026年11月04日 09:34:01   作者:解决问题no解决代码问题  
在接口开发中,手机号、身份证、邮箱等敏感信息需要在返回前端时进行打码脱敏,本文基于 Jackson 序列化扩展机制,实现一套注解驱动的字段级精准脱敏方案,并使用 JDK17 新特性优化 switch 脱敏逻辑,代码更简洁、可读性更强,需要的朋友可以参考下

在接口开发中,手机号、身份证、邮箱等敏感信息需要在返回前端时进行打码脱敏。本文基于 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);
        };
    }
}

三、完整执行流程解析

  1. 启动阶段:SpringBoot 启动时加载 Jackson 自动配置,执行我们的 desensitizeCustomizer,将自定义模块注册到全局 ObjectMapper。
  2. 内省绑定:模块注册时,DesensitizeAnnotationIntrospector 被加入 Jackson 内省器链的首位,优先级高于默认内省器。
  3. 序列化触发:接口返回对象时,SpringMVC 调用 ObjectMapper 将对象序列化为 JSON。
  4. 字段检测:遍历对象每个字段时,Jackson 依次调用内省器链的 findSerializer 方法;我们的内省器优先检测字段是否标注 @Desensitize 且为 String 类型。
  5. 序列化执行:匹配成功则返回 DesensitizeSerializer 实例,进入自定义 serialize 方法,通过 JDK17 switch 表达式执行对应脱敏规则,最终输出脱敏后的字符串。
  6. 兜底逻辑:无注解或非 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注解式字段脱敏的资料请关注脚本之家其它相关文章!

相关文章

  • Java应用CPU飙高排查的实战流程

    Java应用CPU飙高排查的实战流程

    线上告警突然提示 CPU 使用率达到 90%,接口延迟也开始上升,此时你会先重启服务,还是立刻执行 jstack,本文整理一套可以直接用于 Linux 服务器的排查流程,并补充容器、GC 和 Arthas 等常见场景,需要的朋友可以参考下
    2026-08-08
  • Java中Druid连接池连接超时获取不到连接的解决

    Java中Druid连接池连接超时获取不到连接的解决

    这篇文章主要介绍了Java中Druid连接池连接超时获取不到连接的解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-11-11
  • Dockerfile的ADD指令实现远程文件的安全校验下载

    Dockerfile的ADD指令实现远程文件的安全校验下载

    本文主要介绍了Dockerfile的ADD指令实现远程文件的安全校验下载,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2026-04-04
  • Java实现计算图中两个顶点的所有路径

    Java实现计算图中两个顶点的所有路径

    这篇文章主要为大家详细介绍了如何利用Java语言实现计算图中两个顶点的所有路径功能,文中通过示例详细讲解了实现的方法,需要的可以参考一下
    2022-10-10
  • Java数据结构与算法之循环队列的实现

    Java数据结构与算法之循环队列的实现

    循环队列 (Circular Queue) 是一种特殊的队列。循环队列解决了队列出队时需要将所有数据前移一位的问题。本文将带大家详细了解循环队列如何实现,需要的朋友可以参考一下
    2021-12-12
  • Java中springboot搭建html的操作代码

    Java中springboot搭建html的操作代码

    这篇文章主要介绍了Java中springboot搭建html的相关操作,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-08-08
  • java开发线上事故理解RocketMQ异步精髓

    java开发线上事故理解RocketMQ异步精髓

    这篇文章主要介绍了java开发线上事故理解RocketMQ异步精髓
    2022-07-07
  • MyBatisPlus通过ID更新数据为NULL的四种方法

    MyBatisPlus通过ID更新数据为NULL的四种方法

    本文主要介绍了MyBatisPlus通过ID更新数据为NULL的实现方法,包括@TableField注解、UpdateWrapper、lambdaUpdate及全局配置,具有一定的参考价值,感兴趣的可以了解一下
    2025-08-08
  • springboot配置过滤器和多个拦截器、执行顺序(案例详解)

    springboot配置过滤器和多个拦截器、执行顺序(案例详解)

    这篇文章主要介绍了springboot配置过滤器和多个拦截器、执行顺序,在文章开头给大家介绍了过滤器配置的两种方法,创建两个拦截器,重写方法结合实例代码给大家介绍的非常详细,需要的朋友可以参考下
    2023-10-10
  • Mybatis中@Param注解的作用说明

    Mybatis中@Param注解的作用说明

    这篇文章主要介绍了Mybatis中@Param注解的作用说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-03-03

最新评论