Spring Boot 实现数据脱敏:自定义注解 + Jackson 序列化器

一、引言

本人在写用户信息模块的时候,觉得手机号、邮箱等内容不应该直接明文返回给前端。

复制代码
{
    "username": "测试用户",
    "phone":    "13812346789",    ❌明文暴露
    "email":    "test@qq.com"     ❌
}

一旦接口被抓包或日志泄露,用户的隐私就全暴露了。正确的做法是返回脱敏后的数据

复制代码
{
    "username": "测试用户",
    "phone":    "138****6789",    ✅
    "email":    "t***@qq.com"     ✅
}    

阅读网上资料,我觉得把脱敏做到序列化出参的这一层比较好,下面是具体实现

二、核心实现:四个组件

组件1:脱敏类型枚举 SensitiveType

先枚举出所有需要脱敏的字段类型:

复制代码
public enum SensitiveType{
    MOBILe,    //手机号
    EMAIL,     //邮箱
    ID_CARD    //身份证
}

组件2:打码规则 SensitiveUtils

复制代码
public class SensitiveUtils {

    /** 手机号:13812346789 -> 138****6789(保留前3后4) */
    public static String maskMobile(String mobile) {
        if (mobile == null || mobile.length() != 11) {
            return mobile;   // 格式不符时原样返回,不抛异常
        }
        return mobile.substring(0, 3) + "****" + mobile.substring(7);
    }

    /** 邮箱:test@qq.com -> t****@qq.com(保留首字母 + @后面) */
    public static String maskEmail(String email) {
        if (email == null || !email.contains("@")) {
            return email;
        }
        int atIndex = email.indexOf("@");
        return email.substring(0, 1) + "****" + email.substring(atIndex);
    }

    /** 身份证:110101199001011234 -> 110***********1234(保留前3后4) */
    public static String maskIdCard(String idCard) {
        if (idCard == null || idCard.length() != 18) {
            return idCard;
        }
        return idCard.substring(0, 3) + "***********" + idCard.substring(14);
    }
}

组件3:脱敏序列化器 SensitiveJsonSerializer

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

    /** 当前字段的脱敏类型 */
    private SensitiveType type;

    /** 无参构造:Jackson 反射创建实例时调用 */
    public SensitiveJsonSerializer() {
    }

    /** 有参构造:内部使用,携带脱敏类型 */
    private SensitiveJsonSerializer(SensitiveType type) {
        this.type = type;
    }

    /** 核心方法:序列化时打码 */
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers)
            throws IOException {
        if (value == null) {
            gen.writeNull();
            return;
        }
        gen.writeString(mask(value, type));
    }

    /** 关键方法:读取字段上的 @Sensitive 注解,拿到脱敏类型 */
    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) {
        if (property != null) {
            Sensitive annotation = property.getAnnotation(Sensitive.class);
            if (annotation != null) {
                return new SensitiveJsonSerializer(annotation.type());
            }
        }
        return this;
    }

    /** 根据类型调用对应的打码方法 */
    private String mask(String value, SensitiveType type) {
        switch (type) {
            case MOBILE: return SensitiveUtils.maskMobile(value);
            case EMAIL: return SensitiveUtils.maskEmail(value);
            case ID_CARD: return SensitiveUtils.maskIdCard(value);
            default: return value;
        }
    }
}

组件4:脱敏注解 @Sensitive

复制代码
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@JacksonAnnotationsInside                       // 关键:允许组合 @JsonSerialize
@JsonSerialize(using = SensitiveJsonSerializer.class)
public @interface Sensitive {

    /** 脱敏类型,默认手机号 */
    SensitiveType type() default SensitiveType.MOBILE;
}

完成上面四步后,只需要在 vo 字段上标注,序列化时自动生效。

复制代码
public class UserProfileVO{

    @Sensitive(type = SensitiveType.MOBILE)
    private String phone;

    @Sensitive(type = SensitiveType.EMAIL)
    private String email;

    //其他字段...
}

三、完整数据流

复制代码
  数据库(User 表)
   phone = 13812346789(完整值)
        │
        ▼
  User 实体(entity,对应数据库,没有 @Sensitive)
   user.phone = 13812346789
        │
        ▼
  Controller 组装:user.getPhone() 把完整值 copy 进 VO
   vo.phone = 13812346789
        │
        ▼
  返回 Result.success(vo) ------ vo 是 UserProfileVO
   UserProfileVO.phone 字段上有 @Sensitive 注解
        │
        ▼
  Jackson 序列化 UserProfileVO 时
   发现 phone 字段有 @Sensitive(type = MOBILE)
   → 触发 SensitiveJsonSerializer
   → 调用 maskMobile 打码
        │
        ▼
  前端收到 JSON
   phone = "138****6789"

脱敏实际上只发生在序列化出参 时,数据库里存的一直是完整值,脱敏是一个展示的动作,而存储层要保护数据应该使用加密或者哈希

四、两个关键技术点

1、@JacksonAnnotationsInside 是什么?

Jackson默认不认识我们自定义的 @Sensitive 注解,@JacksonAnnotationsInside 的作用是:允许把 @JsonSerialize 组合进自定义注解。

加了它之后,@Sensitive 就等价于 @JsonSerialize(using = SensitiveJsonSerializer.class) ,Jackson才能识别并触发我们的序列化器。

2、ContextualSerializer 有什么用?

序列化器怎么知道是 手机号? 邮箱? 答案是 createContextual 方法:

  • Jackson 序列化某个字段前,会调用 createContextual
  • 它通过 property.getAnnotation(Sensitive.class) 读字段上的注解
  • 拿到 type 后,返回一个携带类型的序列化实例

五、总结

本文实现了一个完整的 Spring 数据脱敏方案,核心链路是:

枚举定义类型 -> 工具类实现规则 -> 序列化器打码 -> 注解标记字段

相关推荐
AI人工智能+电脑小能手1 小时前
大白话说Java设计模式-28-模板方法模式(业务实战篇)
java·设计模式·模板方法模式·spring源码·订单系统·代码复用
奥莱维1 小时前
KNX酒店方案_KNX专用线与高端酒店技术逻辑
java·服务器·前端·数据库
工业一体机老司机1 小时前
Linux工业一体机自启动服务配置:systemd服务单元编写与开机优化实战
java·linux·服务器
weixin_538601972 小时前
智能体测开Day56
java
杨丰玮4182 小时前
从零手写Java飞机躲障碍游戏|Swing绘图、鼠标跟随、计时器碰撞检测实战(五)
java·python·游戏·游戏引擎·图形渲染·动画·贴图
莫得感情 o2 小时前
踩坑 - 压测三轮后 502:一个默认 10 的连接池如何拖垮整个服务
java
Escalating_xu2 小时前
【Linux线程】线程控制全解析:终止、join/detach、cancel、线程栈与 NPTL(下篇)
java·linux·运维
明月_清风2 小时前
vLLM 深度实战:2026 年生产级 LLM 推理引擎完全指南
前端·后端·ai编程
2602_959960922 小时前
电商大厂Java面试实录:Spring Boot/JVM/Redis/Kafka/微服务/安全/测试全解析
java·jvm·spring boot·redis·面试题