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 {
}
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
*/
@Override
public 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
*/
@Override
public 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
*/
@Override
public String desensitize(String value) {
return value;
}
}
import com.wzw.strategy.DesensitizationStrategy;
/**
* 姓名脱敏实现
* 规则:
* - 如果姓名为2个字,只显示第一个字 + *
* - 如果姓名为3个字,显示第一个字 + * + 最后一个字
* - 如果姓名大于3个字,显示第一个字 + ** + 最后一个字
*/
public class NameDesensitizationStrategy implements DesensitizationStrategy {
@Override
public 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 {
@Override
public 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;
}
}
定义两个,是避免序列化冲突,只有手动调用的时候,才使用自定义的序列化脱敏
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
@Bean
public 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;
}
@Override
public 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);
}
}
@Override
public 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();
}
}
请求时通过拦截器设置 ThreadLocal 标记 → 返回时 Jackson 序列化器读取标记并决定是否脱敏
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;
}
@Override
public 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);
}
}
@Override
public 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 {
@Bean
public 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;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
// 清理ThreadLocal,避免内存泄漏
DesensitizeContextHolder.clear();
}
}
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 {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new DesensitizeInterceptor())
.addPathPatterns("/**"); // 拦截所有请求
}
}
反射和jackson都是一样的
@Around("execution(* com.wzw.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 | 高 | 高 | 反射操作、多字段遍历、对象深拷贝或原对象修改 | 性能最差,反射慢、内存开销大、易出错,且不可用于不可变对象 | ⭐ 不推荐生产使用 |