一、引言
本人在写用户信息模块的时候,觉得手机号、邮箱等内容不应该直接明文返回给前端。
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 数据脱敏方案,核心链路是:
枚举定义类型 -> 工具类实现规则 -> 序列化器打码 -> 注解标记字段