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

一、引言

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

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

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

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

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

二、核心实现:四个组件

组件1:脱敏类型枚举 SensitiveType

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

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

组件2:打码规则 SensitiveUtils

typescript 复制代码
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

scala 复制代码
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

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

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

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

typescript 复制代码
public class UserProfileVO{

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

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

    //其他字段...
}

三、完整数据流

ini 复制代码
  数据库(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 数据脱敏方案,核心链路是:

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

相关推荐
金銀銅鐵2 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
念何架构之路2 小时前
go-grpc设计思想
开发语言·后端·golang
system_error_x3 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn4 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈5 小时前
AI 读你的提示词,其实很没耐心
后端
用户8356290780516 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我123456 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒6 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记6 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk598786 小时前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端