文章目錄
- 前言
- 一.配置
- 1.脫敏類型枚舉:DesensitizeType
- 2.注解:Desensitize
- 3.序列化類:DesensitizeJsonSerializer
- 4.工具類:DesensitizeUtil
- 二、測試:DesensitizeTest
- 三、效果展示
- 總結
前言
在互聯網應用中,用戶隱私數據(如姓名、身份證號、手機號、郵箱等)的保護至關重要。為了防止敏感信息在日志、接口返回、前端展示等環節泄露,數據脫敏 成為系統開發中的必備功能
一.配置
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);}@Overridepublic 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));}}/* 讀取注解信息,構造專屬序列化器 */@Overridepublic 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@Builderpublic 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;}@Testpublic 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 項目