文章目录
前言
在互联网应用中,用户隐私数据(如姓名、身份证号、手机号、邮箱等)的保护至关重要。为了防止敏感信息在日志、接口返回、前端展示等环节泄露,数据脱敏 成为系统开发中的必备功能
一.配置
1.脱敏类型枚举:DesensitizeType
import lombok.Getter;
/**
* 脱敏类型枚举
*/
@Getter
public enum DesensitizeType {
NAME(0, "中文姓名"),
ID_CARD(1, "身份证号"),
EMAIL(2, "邮箱"),
PHONE(3, "手机号"),
CUSTOM(4, "自定义");
private final int code;
private final String description;
DesensitizeType(int code, String description) {
this.code = code;
this.description = description;
}
/**
* 通过 code 反查枚举
*/
public static DesensitizeType fromCode(int code) {
for (DesensitizeType type : values()) {
if (type.getCode() == code) {
return type;
}
}
throw new IllegalArgumentException("未知的脱敏类型 code: " + code);
}
}
使用 @Getter 自动生成 getter,fromCode 支持通过 code 查找枚举,便于后续扩展配置化脱敏策略
2.注解:Desensitize
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fc.enums.DesensitizeType;
import com.fc.serializer.DesensitizeJsonSerializer;
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
@JsonSerialize(using = DesensitizeJsonSerializer.class) // 该注解使用序列化的方式
public @interface Desensitize {
DesensitizeType type();//脱敏数据类型(必须指定类型)
int prefixNoMaskLen() default 3; // 手机号通常保留前3位
int suffixNoMaskLen() default 4; // 手机号通常保留后4位
String symbol() default "*";//替换符号
}
3.序列化类:DesensitizeJsonSerializer
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fc.anno.Desensitize;
import com.fc.enums.DesensitizeType;
import com.fc.utils.DesensitizeUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.io.IOException;
import java.util.EnumMap;
import java.util.Objects;
import java.util.function.UnaryOperator;
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DesensitizeJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {
private DesensitizeType type;
private int prefixLen;
private int suffixLen;
private String mask;
/* 策略表:DesensitizeType → 函数 */
private static final EnumMap<DesensitizeType, UnaryOperator<String>> STRATEGY = new EnumMap<>(DesensitizeType.class);
static {
STRATEGY.put(DesensitizeType.NAME, DesensitizeUtil::hideName);
STRATEGY.put(DesensitizeType.ID_CARD, DesensitizeUtil::hideIdCard);
STRATEGY.put(DesensitizeType.EMAIL, DesensitizeUtil::hideEmail);
STRATEGY.put(DesensitizeType.PHONE, DesensitizeUtil::hidePhone);
}
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
if (value == null) { // 1) 空值透传
gen.writeNull();
return;
}
if (type == DesensitizeType.CUSTOM) {
gen.writeString(DesensitizeUtil.customMask(value, prefixLen, suffixLen, mask));
} else {
UnaryOperator<String> func = STRATEGY.get(type);
if (func == null) {
throw new IllegalArgumentException("未注册的策略:" + type);
}
gen.writeString(func.apply(value));
}
}
/* 读取注解信息,构造专属序列化器 */
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
throws JsonMappingException {
if (property != null && Objects.equals(property.getType().getRawClass(), String.class)) {
Desensitize anno = property.getAnnotation(Desensitize.class);
if (anno != null) {
return new DesensitizeJsonSerializer(
anno.type(), anno.prefixNoMaskLen(), anno.suffixNoMaskLen(), anno.symbol());
}
}
return prov.findValueSerializer(property.getType(), property);
}
}
ContextualSerializer
允许在序列化前读取字段的注解信息。
每个带@Desensitize
的字段都会生成一个“专属”序列化器,携带注解参数。
使用EnumMap
存储策略,性能优于 if-else 或 switch。
4.工具类:DesensitizeUtil
import org.apache.commons.lang.StringUtils;
import java.util.regex.Pattern;
/**
* 脱敏工具类
*/
public class DesensitizeUtil {
/* 预编译正则表达式(提升性能) */
private static final Pattern PHONE_PATTERN = Pattern.compile("(\\d{3})\\d{4}(\\d{4})");
private static final Pattern EMAIL_PATTERN = Pattern.compile("(^.)[^@]*(@.*$)");
private static final Pattern ID_CARD_PATTERN = Pattern.compile("(\\d{4})\\d{10}(\\w{4})");
/**
* 中文姓名:张三 → 张*
*/
public static String hideName(String name) {
if (StringUtils.isBlank(name)) return name;
return name.charAt(0) + "*".repeat(Math.max(0, name.length() - 1));
}
/**
* 手机号:13812345678 → 138****5678
*/
public static String hidePhone(String phone) {
if (StringUtils.isBlank(phone)) return phone;
return PHONE_PATTERN.matcher(phone).replaceFirst("$1****$2");
}
/**
* 邮箱:zhangsan@163.com → z****@163.com
*/
public static String hideEmail(String email) {
if (StringUtils.isBlank(email)) return email;
return EMAIL_PATTERN.matcher(email).replaceFirst("$1****$2");
}
/**
* 身份证:123456789012345678 → 1234****5678
*/
public static String hideIdCard(String idCard) {
if (StringUtils.isBlank(idCard)) return idCard;
return ID_CARD_PATTERN.matcher(idCard).replaceFirst("$1****$2");
}
/**
* 通用脱敏:保留前后指定长度,中间用符号填充
*/
public static String customMask(String origin, int prefix, int suffix, String mask) {
if (origin == null || origin.isEmpty()) return origin;
int len = origin.length();
if (prefix + suffix >= len) return origin; // 不脱敏
String prefixStr = origin.substring(0, prefix);
String suffixStr = origin.substring(len - suffix);
String masked = mask.repeat(len - prefix - suffix);
return prefixStr + masked + suffixStr;
}
}
使用
StringUtils.isBlank
来安全判断空值;正则预编译提升性能
二、测试:DesensitizeTest
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fc.anno.Desensitize;
import com.fc.enums.DesensitizeType;
import lombok.Builder;
import lombok.Data;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
public class DesensitizeTest {
@Data
@Builder
public static class UserVO {
private Long id;
private Long roleId;
@Desensitize(type = DesensitizeType.ID_CARD)
private String idCard;
@Desensitize(type = DesensitizeType.NAME)
private String name;
@Desensitize(type = DesensitizeType.PHONE)
private String phoneNumber;
@Desensitize(type = DesensitizeType.EMAIL)
private String email;
@Desensitize(type = DesensitizeType.CUSTOM, prefixNoMaskLen = 2, suffixNoMaskLen = 2, symbol = "#")
private String bankCard;
private String username;
}
@Test
public void testDesensitizeWithInlineVO() throws JsonProcessingException {
// 创建测试数据
UserVO user = UserVO.builder()
.id(1L)
.roleId(101L)
.idCard("110105199003075678")
.name("欧阳锋")
.phoneNumber("13812345678")
.email("ouyangfeng@shaguyin.com")
.bankCard("6228480031567890123")
.username("ofeng")
.build();
// 创建 ObjectMapper(确保注册了自定义序列化器)
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(user);
System.out.println("序列化结果:");
System.out.println(json);
// 断言脱敏效果
assertThat(json).contains("\"idCard\":\"1101****5678\"");
assertThat(json).contains("\"name\":\"欧**\"");
assertThat(json).contains("\"phoneNumber\":\"138****5678\"");
assertThat(json).contains("\"email\":\"o****@shaguyin.com\"");
assertThat(json).contains("\"bankCard\":\"62###############23\"");
assertThat(json).contains("\"username\":\"ofeng\"");
}
}
三、效果展示
import com.fc.anno.Desensitize;
import com.fc.enums.DesensitizeType;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Builder
public class UserVO {
private Long id;
private Long roleId;
@Desensitize(type = DesensitizeType.ID_CARD)
private String idCard;
private String username;
private String name;
private String phoneNumber;
private LocalDateTime createTime;
private LocalDateTime lastLogin;
}
总结
本文实现了一套基于 Jackson 序列化 + 注解 + 策略模式 的轻量级数据脱敏框架,具备以下优点:
✅ 无侵入性:仅需在 VO 字段添加注解
✅ 高性能:使用 EnumMap + 预编译正则
✅ 易扩展:新增类型只需添加枚举和策略
✅ 灵活配置:支持自定义前后缀与掩码符号
✅ 无缝集成:适用于 Spring Boot + Jackson 项目