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
     */
    @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;
    }
}

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
    @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();
    }
}

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;
    }

    @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();
    }

}

拦截器添加到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 {
    @Override
    public 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 反射操作、多字段遍历、对象深拷贝或原对象修改 性能最差,反射慢、内存开销大、易出错,且不可用于不可变对象 ⭐ 不推荐生产使用

你可能感兴趣的:(java,spring,boot,java,spring,脱敏)