springboot数据脱敏(接口级别)
文章目录
- 自定义脱敏注解
- 脱敏注解
- 接口脱敏注解
- 反射+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 | 高 | 高 | 反射操作、多字段遍历、对象深拷贝或原对象修改 | 性能最差,反射慢、内存开销大、易出错,且不可用于不可变对象 | ⭐ 不推荐生产使用 |