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 数据脱敏方案,核心链路是:

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

相关推荐
码事漫谈8 小时前
多人共用一个 key,缓存命中率会不会因此降低?
后端
考虑考虑9 小时前
docker compose环境变量替换
运维·后端·自动化运维
武雄(小星Ai)10 小时前
飞书API上传26MB文件偶发失败:错误码9499与空响应体排坑实录
后端·api·排坑实录
徐小夕10 小时前
JitWord 4.0 万字分享:从协同工具到AI Word操作系统,聊聊3年产品创业史
前端·vue.js·后端
geovindu11 小时前
CSharp: Observer Pattern
开发语言·后端·观察者模式·设计模式·c#·.netcore·行为模式
Flynt12 小时前
把公司项目迁到 Spring Boot 4.0:编译通过只是开始
java·spring boot·后端
IT_陈寒12 小时前
Redis误用keys命令把生产环境搞崩了,血的教训
前端·人工智能·后端
她的男孩13 小时前
多租户和数据权限怎么共存?扒完拦截器注册链路,我找到 4 个隐蔽的坑
java·后端·架构
用户83562907805113 小时前
使用 Python 设置 Excel 页眉和页脚
后端·python
步行cgn13 小时前
IoC 控制反转:从概念到 Spring 的实现
后端