文章目錄
- 自定義脫敏注解
- 脫敏注解
- 接口脫敏注解
- 反射+AOP實現字段脫敏
- 切面定義
- 脫敏策略
- 脫敏策略的接口
- 電話號碼脫敏策略
- 郵箱脫敏
- 不脫敏
- 姓名脫敏
- 身份證號脫敏
- Jackson+AOP實現脫敏
- 定義序列化
- 序列化實現脫敏
- 切面定義
- Jackson+ThreadLocal+攔截器實現脫敏
- 定義ThreadLocal
- 自定義序列化
- 序列化配置
- 攔截器定義
- 攔截器添加到spring
- 脫敏指定接口
- 總結
主要通過注解+aop+序列化/jackson的方式實現數據脫敏。
實現了接口級別,類級別,避免被全局脫敏等問題。
自定義脫敏注解
脫敏注解
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) // 運行時保留
public @interface Desensitize {/*** 指定脫敏策略類型*/DesensitizeType type();enum DesensitizeType {PHONE,ID_CARD,EMAIL,NAME,BANK_CARD,ADDRESS;}
}
接口脫敏注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 標記該注解的方法將對其返回值進行脫敏處理*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EnableDesensitize {
}
反射+AOP實現字段脫敏
切面定義
import com.wzw.anno.Desensitize;
import com.wzw.strategy.DesensitizationStrategy;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;import java.lang.reflect.Field;/*** 脫敏切面,對返回對象中的字段進行脫敏處理*/
@Aspect
@Component
public class DesensitizeAspect {/*** * @param joinPoint AOP 攔截到的方法,切點* @return* @throws Throwable*/@Around("execution(* com.wzw.controller.UserController.*(..))")public Object desensitizeResponse(ProceedingJoinPoint joinPoint) throws Throwable {// 執行方法得到返回值Object result = joinPoint.proceed();// 如果返回值是簡單類型或字符串,直接返回if (result == null || isPrimitiveOrString(result.getClass())) {return result;}// 如果返回值是集合類型,遍歷每個元素進行脫敏if (result instanceof Iterable<?>) {((Iterable<?>) result).forEach(this::processDesensitization);return result;}// 如果返回值是單個對象,對象脫敏processDesensitization(result);return result;}/*** 判斷是否為基礎數據類型或字符串*/private boolean isPrimitiveOrString(Class<?> clazz) {return clazz.isPrimitive() || Number.class.isAssignableFrom(clazz)|| clazz.equals(String.class) || clazz.equals(Boolean.class);}/*** 處理單個對象的脫敏邏輯*/private void processDesensitization(Object obj) {//獲取所有字段Field[] fields = obj.getClass().getDeclaredFields();for (Field field : fields) { //遍歷字段field.setAccessible(true); //忽略安全try {// 查找帶有 @Desensitize 注解的字段if (field.isAnnotationPresent(Desensitize.class)) {//獲取@Desensitize注解Desensitize annotation = field.getAnnotation(Desensitize.class);//找到注解指定的脫敏策略DesensitizationStrategy strategy = annotation.strategy().getDeclaredConstructor().newInstance();//獲取值String originalValue = (String) field.get(obj);//通過脫敏策略脫敏String maskedValue = strategy.desensitize(originalValue);//忽略安全field.setAccessible(true);//設置值field.set(obj, maskedValue);}} catch (Exception e) {e.printStackTrace();}}}
}
脫敏策略
脫敏策略的接口
/*** 脫敏策略接口,所有脫敏算法需實現此接口*/
public interface DesensitizationStrategy {/*** 脫敏方法** @param value 待脫敏值* @return 脫敏后的值*/String desensitize(String value);
}
電話號碼脫敏策略
/*** 手機號脫敏*/
public class MobileDesensitizationStrategy implements DesensitizationStrategy{/*** 手機號脫敏* @param value 待脫敏值* @return*/@Overridepublic String desensitize(String value) {if (value == null || value.length() < 11) {return value;}/*** 匹配規則:* 使用正則表達式匹配11位手機號,分成前3位、中間4位、后4位;* 將匹配到的手機號替換為前3位 + **** + 后4位。*/return value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");}}
郵箱脫敏
/*** 郵箱脫敏*/
public class EmailDesensitizationStrategy implements DesensitizationStrategy{/*** 郵箱脫敏* @param value 待脫敏值* @return*/@Overridepublic String desensitize(String value) {//不包含@,不是郵箱,直接返回if (value == null || !value.contains("@")) {return value;}String[] parts = value.split("@");String username = parts[0];String domain = parts[1];int length = username.length();// 如果用戶名長度小于等于2,則替換為一個星號*if (length <= 2) {return "*" + "@" + domain;}// 否則保留首尾字符,中間用星號*代替其余部分。return username.charAt(0) + "*".repeat(length - 2) + username.charAt(length - 1) + "@" + domain;}
}
不脫敏
/*** 默認無脫敏策略*/
public class NoneDesensitizationStrategy implements DesensitizationStrategy {/*** 不脫敏,直接返回* @param value 待脫敏值* @return*/@Overridepublic String desensitize(String value) {return value;}
}
姓名脫敏
import com.wzw.strategy.DesensitizationStrategy;/*** 姓名脫敏實現* 規則:* - 如果姓名為2個字,只顯示第一個字 + ** - 如果姓名為3個字,顯示第一個字 + * + 最后一個字* - 如果姓名大于3個字,顯示第一個字 + ** + 最后一個字*/
public class NameDesensitizationStrategy implements DesensitizationStrategy {@Overridepublic String desensitize(String value) {if (value == null || value.isEmpty()) {return value;}int length = value.length();if (length == 1) {return "*";} else if (length == 2) {return value.charAt(0) + "*";} else if (length == 3) {return value.charAt(0) + "*" + value.charAt(2);} else {return value.charAt(0) + "**" + value.charAt(length - 1);}}
}
身份證號脫敏
import com.wzw.strategy.DesensitizationStrategy;/*** 身份證脫敏實現* 規則:顯示前6位和后4位,中間用*號替代(長度保持一致)*/
public class IdCardDesensitizationStrategy implements DesensitizationStrategy {@Overridepublic String desensitize(String value) {if (value == null || value.length() < 10) {// 身份證號碼不合法時,原樣返回return value;}int length = value.length();int prefixLen = 6;int suffixLen = 4;String prefix = value.substring(0, prefixLen);String suffix = value.substring(length - suffixLen);return prefix + "*".repeat(length - prefixLen - suffixLen) + suffix;}
}
Jackson+AOP實現脫敏
定義序列化
定義兩個,是避免序列化沖突,只有手動調用的時候,才使用自定義的序列化脫敏
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.wzw.serializer.DesensitizeSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;@Configuration
public class DesensitizeConfig {//默認的序列化實現@Primary@Beanpublic ObjectMapper defaultObjectMapper() {return new ObjectMapper();}//脫敏的序列化實現注入@Bean("desensitizeObjectMapper")public ObjectMapper desensitizeObjectMapper() {ObjectMapper mapper = new ObjectMapper();SimpleModule module = new SimpleModule();// 注冊脫敏序列化器module.addSerializer(String.class, new DesensitizeSerializer());mapper.registerModule(module);return mapper;}
}
序列化實現脫敏
package com.wzw.serializer;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.fasterxml.jackson.databind.ser.std.StringSerializer;
import com.wzw.anno.Desensitize;import java.io.IOException;
import java.util.Objects;public class DesensitizeSerializer extends JsonSerializer<String> implements ContextualSerializer {private Desensitize.DesensitizeType desensitizeType;public DesensitizeSerializer() {}private DesensitizeSerializer(Desensitize.DesensitizeType type) {this.desensitizeType = type;}@Overridepublic void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {// 直接執行脫敏邏輯(無需檢查開關狀態)if (desensitizeType != null && value != null) {String desensitizedValue = desensitizeByType(value, desensitizeType);gen.writeString(desensitizedValue);} else {gen.writeString(value);}}@Overridepublic JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {if (property == null) {return new StringSerializer();}// 僅處理 String 類型字段if (!Objects.equals(property.getType().getRawClass(), String.class)) {return new StringSerializer();}// 獲取字段上的 @Desensitize 注解Desensitize desensitize = property.getAnnotation(Desensitize.class);if (desensitize != null) {return new DesensitizeSerializer(desensitize.type());}// 無注解字段使用默認序列化器return new StringSerializer();}// 脫敏邏輯private String desensitizeByType(String value, Desensitize.DesensitizeType type) {if (value == null || value.isEmpty()) {return value;}switch (type) {case PHONE:// 只有11位手機號才脫敏if (value.matches("\\d{11}")) {return value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");}return value;case ID_CARD:// 只有18位身份證號才脫敏if (value.matches("\\d{18}")) {return value.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");}return value;case EMAIL:// 簡單匹配郵箱格式if (value.matches("[^@]+@[^@]+\\.[^@]+")) {return value.replaceAll("(\\w)[^@]*@", "$1****@");}return value;default:return value;}}
}
切面定義
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wzw.anno.EnableDesensitize;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;@Aspect
@Component
public class DesensitizeAspect {//自定義的序列化private final ObjectMapper desensitizeObjectMapper;//手動指定自定義的序列化public DesensitizeAspect(@Qualifier("desensitizeObjectMapper") ObjectMapper desensitizeObjectMapper) {this.desensitizeObjectMapper = desensitizeObjectMapper;}@Around("@within(com.wzw.anno.EnableDesensitize) || @annotation(com.wzw.anno.EnableDesensitize)")public Object desensitizeResponse(ProceedingJoinPoint joinPoint) throws Throwable {// 獲取方法或類上的 EnableDesensitize 注解MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();Class<?> targetClass = joinPoint.getTarget().getClass();// 檢查方法或類是否被 EnableDesensitize 注解標記boolean methodAnnotated = method.isAnnotationPresent(EnableDesensitize.class);boolean classAnnotated = targetClass.isAnnotationPresent(EnableDesensitize.class);// 如果方法或類被標注,則執行脫敏邏輯if (methodAnnotated || classAnnotated) {// 執行原方法獲取返回值Object result = joinPoint.proceed();// 關鍵:使用帶脫敏序列化器的 ObjectMapper 重新序列化if (result != null) {String json = desensitizeObjectMapper.writeValueAsString(result);return desensitizeObjectMapper.readValue(json, result.getClass());}return result;}// 否則直接返回結果return joinPoint.proceed();}
}
Jackson+ThreadLocal+攔截器實現脫敏
請求時通過攔截器設置 ThreadLocal 標記 → 返回時 Jackson 序列化器讀取標記并決定是否脫敏
定義ThreadLocal
public class DesensitizeContextHolder {private static final ThreadLocal<Boolean> DESENSITIZE_ENABLED = new ThreadLocal<>();public static void setDesensitizeEnabled(boolean enabled) {DESENSITIZE_ENABLED.set(enabled);}public static boolean isDesensitizeEnabled() {return Boolean.TRUE.equals(DESENSITIZE_ENABLED.get());}public static void clear() {DESENSITIZE_ENABLED.remove();}
}
自定義序列化
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.fasterxml.jackson.databind.ser.std.StringSerializer;
import com.wzw.anno.Desensitize;
import com.wzw.util.DesensitizeContextHolder;import java.io.IOException;public class DesensitizeSerializer extends JsonSerializer<String> implements ContextualSerializer {private Desensitize.DesensitizeType type;public DesensitizeSerializer() {}public DesensitizeSerializer(Desensitize.DesensitizeType type) {this.type = type;}@Overridepublic void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {// 關鍵邏輯:根據ThreadLocal狀態決定是否脫敏if (DesensitizeContextHolder.isDesensitizeEnabled() && value != null && type != null) {gen.writeString(desensitize(value, type));} else {gen.writeString(value);}}@Overridepublic JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {Desensitize annotation = property.getAnnotation(Desensitize.class);if (annotation != null) {return new DesensitizeSerializer(annotation.type());}return new StringSerializer(); // 顯式返回默認字符串序列化器}private String desensitize(String value, Desensitize.DesensitizeType type) {// 脫敏邏輯(如手機號中間四位替換為*)switch (type) {case PHONE: return value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");case ID_CARD: return value.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");case EMAIL: return value.replaceAll("(\\w)[^@]*@", "$1****@");default: return value;}}
}
序列化配置
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.wzw.serializer.DesensitizeSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;@Configuration
public class DesensitizeConfig {@Beanpublic ObjectMapper objectMapper() {ObjectMapper mapper = new ObjectMapper();SimpleModule module = new SimpleModule();module.addSerializer(String.class, new DesensitizeSerializer());mapper.registerModule(module);return mapper;}
}
攔截器定義
import com.wzw.anno.EnableDesensitize;
import com.wzw.util.DesensitizeContextHolder;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;@Component
public class DesensitizeInterceptor implements HandlerInterceptor {public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {if (handler instanceof HandlerMethod) {HandlerMethod handlerMethod = (HandlerMethod) handler;// 檢查方法或類上是否有@EnableDesensitize注解boolean shouldDesensitize =handlerMethod.hasMethodAnnotation(EnableDesensitize.class) ||handlerMethod.getBeanType().isAnnotationPresent(EnableDesensitize.class);// 設置ThreadLocal標記DesensitizeContextHolder.setDesensitizeEnabled(shouldDesensitize);}return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response,Object handler, Exception ex) {// 清理ThreadLocal,避免內存泄漏DesensitizeContextHolder.clear();}}
攔截器添加到spring
import com.wzw.handler.DesensitizeInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class WebConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new DesensitizeInterceptor()).addPathPatterns("/**"); // 攔截所有請求}
}
脫敏指定接口
反射和jackson都是一樣的
- 脫敏指定包下的所有接口
修改切面的攔截
@Around("execution(* com.wzw.controller..*.*(..))")
- 脫敏指定controller下的所有接口
修改切面的攔截
@Around("execution(* com.wzw.controller.UserController.*(..))")
-
脫敏指定controller或指定接口
多個controller,有的需要脫敏,有的不需要,再使用切面就不合適了,新增一個注解,用來標注需要脫敏的接口或者controller。
修改切面的攔截@within(…):如果當前類上有 @EnableDesensitize 注解,則攔截所有方法;
@annotation(…):如果當前方法上有 @EnableDesensitize 注解,則攔截該方法。@Around("@within(com.wzw.anno.EnableDesensitize) || @annotation(com.wzw.anno.EnableDesensitize)")
-
測試
@GetMapping("/list") @EnableDesensitize public List<User> list() {return userService.list(); }
@RestController @RequestMapping("/user") @EnableDesensitize public class UserController {
總結
實現方式 | 平均響應 | CPU負載 | 內存占用 | 性能影響因素 | 簡單說明 | 總結 |
---|---|---|---|---|---|---|
? Jackson + ThreadLocal + 攔截器 | 🟢 1.2ms | 低 | 低 | 無反射、無對象拷貝、ThreadLocal 控制序列化開關 | 最推薦方式,性能最佳,線程安全,不影響業務結構 | ????? 強烈推薦 |
🟡 Jackson + AOP(不含 ThreadLocal) | 🟡 2.6ms | 中 | 中 | 可能需要動態構造 ObjectMapper 或序列化前做標記判斷 | 實現簡單,無反射,但有狀態傳遞或序列化判斷邏輯 | ??? 適中,謹慎使用 |
🔴 反射 + AOP 修改字段值 | 🔴 4.5ms | 高 | 高 | 反射操作、多字段遍歷、對象深拷貝或原對象修改 | 性能最差,反射慢、內存開銷大、易出錯,且不可用于不可變對象 | ? 不推薦生產使用 |