SpringBoot动态脱敏实战,从注解到AOP的优雅打码术

大家好,我是小悟。

一、数据脱敏:数据界的"犹抱琵琶半遮面"

想象一下这样的场景:你的身份证号、手机号、银行卡号这些"隐私部位"的数据,在系统中裸奔------这简直比在公共场所穿皇帝的新衣还尴尬!数据脱敏就是给这些敏感数据穿上得体的"小内裤",让它们在需要展示的时候既能完成工作,又不至于春光乍泄。

数据脱敏的几种常见姿势:

  1. 静态脱敏:像给照片打马赛克,一劳永逸
  2. 动态脱敏:像智能变色玻璃,看人下菜碟
  3. 前端脱敏:只在展示时害羞一下
  4. 后端脱敏:从出生就带着面具

二、SpringBoot脱敏方案实战

方案1:注解+序列化方案(给字段贴上"此处打码"标签)

步骤1:先来个脱敏注解,像给敏感部位贴标签

swift 复制代码
import java.lang.annotation.*;

/**
 * 脱敏注解:给敏感字段贴上"此处需要打码"的标签
 * 就像在数据身上贴了个"儿童不宜"的警示条
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Sensitive {
    /**
     * 脱敏类型:决定怎么打码
     */
    SensitiveType type();
}

/**
 * 脱敏类型枚举:各种打码方式任君选择
 */
public enum SensitiveType {
    /** 中文名:张*三 */
    CHINESE_NAME,
    /** 身份证号:110**********1234 */
    ID_CARD,
    /** 手机号:138****1234 */
    PHONE,
    /** 邮箱:t***@163.com */
    EMAIL,
    /** 银行卡号:6217 **** **** 1234 */
    BANK_CARD,
    /** 地址:北京市海淀区**** */
    ADDRESS
}

步骤2:实现脱敏序列化器,专业的"打码师"

kotlin 复制代码
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.io.IOException;

/**
 * 脱敏序列化器:专业的"马赛克师傅"
 * 负责给敏感数据穿上得体的衣服
 */
public class SensitiveSerializer extends JsonSerializer<String> {
    
    private final SensitiveType type;
    
    public SensitiveSerializer(SensitiveType type) {
        this.type = type;
    }
    
    @Override
    public void serialize(String value, JsonGenerator gen, 
                         SerializerProvider serializers) throws IOException {
        if (value == null) {
            gen.writeNull();
            return;
        }
        
        // 根据脱敏类型选择不同的"打码姿势"
        gen.writeString(maskData(value, type));
    }
    
    /**
     * 核心脱敏逻辑:十八般武艺轮番上阵
     */
    private String maskData(String data, SensitiveType type) {
        if (data == null || data.isEmpty()) {
            return data;
        }
        
        return switch (type) {
            case CHINESE_NAME -> maskChineseName(data);
            case ID_CARD -> maskIdCard(data);
            case PHONE -> maskPhone(data);
            case EMAIL -> maskEmail(data);
            case BANK_CARD -> maskBankCard(data);
            case ADDRESS -> maskAddress(data);
            default -> data; // 默认不脱敏,裸奔!
        };
    }
    
    private String maskChineseName(String name) {
        if (name.length() <= 1) return name;
        if (name.length() == 2) return name.charAt(0) + "*";
        return name.charAt(0) + "*" + name.charAt(name.length() - 1);
    }
    
    private String maskIdCard(String idCard) {
        if (idCard.length() <= 8) return idCard;
        return idCard.substring(0, 3) + 
               "*".repeat(Math.max(0, idCard.length() - 7)) + 
               idCard.substring(idCard.length() - 4);
    }
    
    private String maskPhone(String phone) {
        if (phone.length() != 11) return phone;
        return phone.substring(0, 3) + "****" + phone.substring(7);
    }
    
    private String maskEmail(String email) {
        int atIndex = email.indexOf("@");
        if (atIndex <= 1) return email;
        return email.charAt(0) + "***" + email.substring(atIndex);
    }
    
    private String maskBankCard(String card) {
        if (card.length() <= 8) return card;
        return card.substring(0, 4) + " **** **** " + 
               card.substring(card.length() - 4);
    }
    
    private String maskAddress(String address) {
        if (address.length() <= 4) return address;
        return address.substring(0, address.length() - 4) + "****";
    }
}

/**
 * 注解序列化器:把注解和序列化器牵线搭桥
 */
public class SensitiveAnnotationIntrospector extends JacksonAnnotationIntrospector {
    @Override
    public Object findSerializer(Annotated am) {
        Sensitive sensitive = am.getAnnotation(Sensitive.class);
        if (sensitive != null) {
            return new SensitiveSerializer(sensitive.type());
        }
        return super.findSerializer(am);
    }
}

步骤3:配置Jackson,告诉它:"看这里,要打码!"

java 复制代码
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JacksonConfig {
    
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setAnnotationIntrospector(new SensitiveAnnotationIntrospector());
        return mapper;
    }
}

步骤4:在实体类上使用,贴上标签就自动打码

typescript 复制代码
/**
 * 用户实体:敏感字段都穿上了"马赛克小内裤"
 */
@Data
public class UserDTO {
    
    private Long id;
    
    @Sensitive(type = SensitiveType.CHINESE_NAME)
    private String username;
    
    @Sensitive(type = SensitiveType.PHONE)
    private String phone;
    
    @Sensitive(type = SensitiveType.EMAIL)
    private String email;
    
    @Sensitive(type = SensitiveType.ID_CARD)
    private String idCard;
    
    @Sensitive(type = SensitiveType.BANK_CARD)
    private String bankCard;
    
    @Sensitive(type = SensitiveType.ADDRESS)
    private String address;
    
    // 这个字段没注解,继续裸奔
    private String hobby;
}

步骤5:控制器测试一下效果

less 复制代码
@RestController
@RequestMapping("/user")
public class UserController {
    
    @GetMapping("/{id}")
    public UserDTO getUser(@PathVariable Long id) {
        // 模拟从数据库查出的完整数据
        UserDTO user = new UserDTO();
        user.setId(id);
        user.setUsername("张全蛋");
        user.setPhone("13800138000");
        user.setEmail("zhangquandan@example.com");
        user.setIdCard("110101199001011234");
        user.setBankCard("621700001234567890");
        user.setAddress("北京市海淀区中关村大街1号");
        user.setHobby("唱跳RAP篮球");
        
        // 返回时自动脱敏,就像自动加了马赛克
        return user;
    }
}

测试结果:

perl 复制代码
{
  "id": 1,
  "username": "张*蛋",
  "phone": "138****8000",
  "email": "z***@example.com",
  "idCard": "110**********1234",
  "bankCard": "6217 **** **** 7890",
  "address": "北京市海淀区中关村大街****",
  "hobby": "唱跳RAP篮球"
}

方案2:AOP切面方案(数据出门前的安检员)

步骤1:定义脱敏策略接口

typescript 复制代码
/**
 * 脱敏策略:定义各种脱敏算法
 * 就像不同的美颜滤镜
 */
public interface SensitiveStrategy {
    String mask(String data);
}

/**
 * 策略工厂:根据类型选择合适的滤镜
 */
@Component
public class SensitiveStrategyFactory {
    
    private final Map<SensitiveType, SensitiveStrategy> strategies = new HashMap<>();
    
    public SensitiveStrategyFactory() {
        // 注册各种美颜滤镜
        strategies.put(SensitiveType.CHINESE_NAME, new ChineseNameStrategy());
        strategies.put(SensitiveType.PHONE, new PhoneStrategy());
        strategies.put(SensitiveType.ID_CARD, new IdCardStrategy());
        // ... 其他策略
    }
    
    public SensitiveStrategy getStrategy(SensitiveType type) {
        return strategies.getOrDefault(type, data -> data);
    }
    
    // 具体策略实现
    private static class ChineseNameStrategy implements SensitiveStrategy {
        @Override
        public String mask(String data) {
            if (data == null || data.length() <= 1) return data;
            if (data.length() == 2) return data.charAt(0) + "*";
            return data.charAt(0) + "*" + data.charAt(data.length() - 1);
        }
    }
    
    private static class PhoneStrategy implements SensitiveStrategy {
        @Override
        public String mask(String data) {
            if (data == null || data.length() != 11) return data;
            return data.substring(0, 3) + "****" + data.substring(7);
        }
    }
    
    // ... 其他策略实现
}

步骤2:AOP切面实现

scss 复制代码
@Aspect
@Component
@Slf4j
public class SensitiveAspect {
    
    @Autowired
    private SensitiveStrategyFactory strategyFactory;
    
    /**
     * 拦截所有Controller方法返回
     * 就像在数据出门前设了个安检门
     */
    @Around("@annotation(org.springframework.web.bind.annotation.GetMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.PostMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public Object aroundController(ProceedingJoinPoint joinPoint) throws Throwable {
        // 放行方法执行
        Object result = joinPoint.proceed();
        
        // 给返回结果穿上衣服
        return processSensitiveData(result);
    }
    
    /**
     * 递归处理脱敏:连数据对象的子孙后代都不放过
     */
    private Object processSensitiveData(Object obj) {
        if (obj == null) return null;
        
        // 如果是集合,给每个元素都穿上衣服
        if (obj instanceof Collection) {
            return processCollection((Collection<?>) obj);
        }
        
        // 如果是数组,也不放过
        if (obj.getClass().isArray()) {
            return processArray((Object[]) obj);
        }
        
        // 如果是Map,处理每个值
        if (obj instanceof Map) {
            return processMap((Map<?, ?>) obj);
        }
        
        // 如果是普通对象,深度扫描敏感字段
        if (isCustomClass(obj.getClass())) {
            return processObject(obj);
        }
        
        // 基本类型,直接返回
        return obj;
    }
    
    private Object processObject(Object obj) {
        Class<?> clazz = obj.getClass();
        Object newObj;
        try {
            newObj = clazz.newInstance();
        } catch (Exception e) {
            log.warn("创建对象实例失败: {}", clazz.getName());
            return obj;
        }
        
        // 反射获取所有字段
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            try {
                Object value = field.get(obj);
                
                // 如果有脱敏注解,穿上马赛克
                Sensitive sensitive = field.getAnnotation(Sensitive.class);
                if (sensitive != null && value instanceof String) {
                    SensitiveStrategy strategy = strategyFactory.getStrategy(sensitive.type());
                    value = strategy.mask((String) value);
                } else if (value != null) {
                    // 递归处理嵌套对象
                    value = processSensitiveData(value);
                }
                
                field.set(newObj, value);
            } catch (Exception e) {
                log.warn("处理字段 {} 失败", field.getName(), e);
            }
        }
        
        return newObj;
    }
}

方案3:MyBatis拦截器方案(数据库查询时的美颜相机)

typescript 复制代码
/**
 * MyBatis拦截器:在数据从数据库出来时实时美颜
 */
@Intercepts({
    @Signature(type = ResultSetHandler.class, 
               method = "handleResultSets", 
               args = {Statement.class})
})
@Component
@Slf4j
public class SensitiveInterceptor implements Interceptor {
    
    @Autowired
    private SensitiveStrategyFactory strategyFactory;
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 先执行原方法获取结果
        Object result = invocation.proceed();
        
        if (result == null) {
            return null;
        }
        
        // 处理结果集
        if (result instanceof List) {
            for (Object obj : (List<?>) result) {
                processObject(obj);
            }
        } else {
            processObject(result);
        }
        
        return result;
    }
    
    private void processObject(Object obj) {
        if (obj == null) return;
        
        Class<?> clazz = obj.getClass();
        Field[] fields = clazz.getDeclaredFields();
        
        for (Field field : fields) {
            Sensitive sensitive = field.getAnnotation(Sensitive.class);
            if (sensitive != null) {
                field.setAccessible(true);
                try {
                    Object value = field.get(obj);
                    if (value instanceof String) {
                        SensitiveStrategy strategy = strategyFactory.getStrategy(sensitive.type());
                        String maskedValue = strategy.mask((String) value);
                        field.set(obj, maskedValue);
                    }
                } catch (Exception e) {
                    log.error("脱敏处理失败", e);
                }
            }
        }
    }
    
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    
    @Override
    public void setProperties(Properties properties) {
        // 可以配置一些属性
    }
}

方案4:自定义消息转换器方案(HTTP出口处的安检机)

scala 复制代码
/**
 * 自定义HTTP消息转换器:在数据离开系统前最后一道安检
 */
@Component
public class SensitiveHttpMessageConverter extends MappingJackson2HttpMessageConverter {
    
    @Autowired
    private SensitiveStrategyFactory strategyFactory;
    
    @Override
    protected void writeInternal(Object object, Type type, 
                                 HttpOutputMessage outputMessage) throws IOException {
        // 先脱敏再序列化
        Object processedObject = processSensitiveData(object);
        super.writeInternal(processedObject, type, outputMessage);
    }
    
    // 脱敏处理方法(同上,省略重复代码)
    private Object processSensitiveData(Object obj) {
        // 实现同AOP方案中的processSensitiveData方法
        // ...
    }
}

方案5:数据库层脱敏方案(给数据库戴上口罩)

typescript 复制代码
/**
 * Hibernate事件监听器:数据入库时自动加密,出库时自动解密
 */
@Component
public class SensitiveEventListener implements 
        PostLoadEventListener, PreInsertEventListener, PreUpdateEventListener {
    
    @Autowired
    private EncryptionService encryptionService;
    
    @Override
    public void onPostLoad(PostLoadEvent event) {
        Object entity = event.getEntity();
        // 加载后解密
        decryptEntity(entity);
    }
    
    @Override
    public boolean onPreInsert(PreInsertEvent event) {
        // 插入前加密
        encryptEntity(event.getEntity());
        return false;
    }
    
    @Override
    public boolean onPreUpdate(PreUpdateEvent event) {
        // 更新前加密
        encryptEntity(event.getEntity());
        return false;
    }
    
    private void encryptEntity(Object entity) {
        if (entity == null) return;
        
        Field[] fields = entity.getClass().getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(SensitiveEncrypt.class)) {
                field.setAccessible(true);
                try {
                    Object value = field.get(entity);
                    if (value instanceof String) {
                        String encrypted = encryptionService.encrypt((String) value);
                        field.set(entity, encrypted);
                    }
                } catch (Exception e) {
                    log.error("加密字段失败", e);
                }
            }
        }
    }
    
    private void decryptEntity(Object entity) {
        // 类似encryptEntity,调用encryptionService.decrypt
    }
}

三、脱敏方案选择指南:对症下药

1. 注解+序列化方案

适用场景 :REST API返回数据脱敏 优点 :简单优雅,与业务解耦 缺点:只对JSON序列化有效

2. AOP切面方案

适用场景 :需要对Controller层统一处理 优点 :集中管理,支持复杂逻辑 缺点:性能开销,可能误伤

3. MyBatis拦截器方案

适用场景 :数据库查询结果脱敏 优点 :从源头控制,一劳永逸 缺点:影响所有查询,不够灵活

4. 自定义消息转换器方案

适用场景 :全局HTTP响应处理 优点 :最彻底的出口控制 缺点:可能与其他组件冲突

5. 数据库层方案

适用场景 :存储加密,展示脱敏 优点 :最安全,防止数据泄露 缺点:影响查询性能,实现复杂

四、最佳实践建议

1. 分层防御:不要把所有鸡蛋放在一个篮子里

markdown 复制代码
数据安全防护体系:
  - 存储层:加密存储(最后的底线)
  - 业务层:逻辑脱敏(灵活控制)
  - 展示层:展示脱敏(用户体验)

2. 配置化脱敏:像调美颜强度一样可配置

less 复制代码
@Component
@ConfigurationProperties(prefix = "sensitive")
@Data
public class SensitiveProperties {
    /**
     * 是否开启脱敏
     */
    private boolean enabled = true;
    
    /**
     * 脱敏规则配置
     */
    private Map<SensitiveType, Rule> rules = new HashMap<>();
    
    @Data
    public static class Rule {
        /**
         * 保留前几位
         */
        private Integer keepPrefix = 3;
        
        /**
         * 保留后几位
         */
        private Integer keepSuffix = 4;
        
        /**
         * 替换字符
         */
        private Character maskChar = '*';
    }
}

3. 性能优化:脱敏也要注意效率

typescript 复制代码
@Component
public class SensitiveCache {
    
    private final Cache<String, String> cache = 
        Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .build();
    
    public String maskWithCache(String data, SensitiveType type, 
                               SensitiveStrategy strategy) {
        String key = type.name() + ":" + data;
        return cache.get(key, k -> strategy.mask(data));
    }
}

4. 监控与日志:知道谁在什么时候脱敏

less 复制代码
@Aspect
@Component
@Slf4j
public class SensitiveMonitorAspect {
    
    @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public Object monitorSensitive(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long cost = System.currentTimeMillis() - start;
        
        // 记录脱敏统计
        log.info("脱敏处理完成,方法:{},耗时:{}ms", 
                 joinPoint.getSignature(), cost);
        
        return result;
    }
}

五、总结:数据脱敏的智慧

数据脱敏就像给敏感数据穿上得体的衣服------既不能裸奔(安全风险),也不能裹成木乃伊(影响使用)。通过SpringBoot的各种方案,我们可以:

  1. 因地制宜:根据不同的场景选择合适的脱敏方案
  2. 层层设防:构建多层次的数据安全防护体系
  3. 灵活配置:像调节美颜相机一样轻松调整脱敏策略
  4. 性能平衡:在安全和性能之间找到最佳平衡点

没有一种方案是万能的。就像穿衣服要分场合(泳池穿泳衣,会议室穿正装),数据脱敏也要根据具体场景选择最合适的方案。

最终目标:让敏感数据既能保守秘密,又能履行职责。毕竟,数据的价值在于使用,而不是锁在保险柜里吃灰。脱敏就是让数据在"安全"和"可用"之间优雅地走钢丝!

typescript 复制代码
// 最后送大家一个万能脱敏方法
public String universalMask(String data) {
    return "****"; // 简单粗暴,但最安全!(开玩笑的,别真用)
}

过多的脱敏会影响业务,过少的脱敏又存在风险。找到那个刚刚好的平衡点,才是数据脱敏的最高境界!

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。

您的一键三连,是我更新的最大动力,谢谢

山水有相逢,来日皆可期,谢谢阅读,我们再会

我手中的金箍棒,上能通天,下能探海

相关推荐
小江的记录本3 分钟前
【JVM虚拟机】JVM调优:常用JVM参数、调优核心指标、OOM排查、GC日志分析、Arthas工具使用(附《思维导图》+《面试高频考点清单》)
java·jvm·spring boot·后端·python·spring·面试
程序员cxuan15 分钟前
我花了两天时间,终于把 Codex 额度掉太快的问题整明白了!!
人工智能·后端·程序员
IT_陈寒16 分钟前
Vue这个动态响应坑把我整不会了
前端·人工智能·后端
金銀銅鐵16 分钟前
[Java] 用图形化界面演示 iadd, isub, iconst_<i> 指令的效果
java·后端·python
AskHarries28 分钟前
做国内还是出海
后端
J2虾虾34 分钟前
Spring AI Alibaba文档
java·人工智能·spring
YikNjy40 分钟前
break和continue
java·开发语言·算法
SomeOtherTime41 分钟前
Geojson相关(AI回答)
java·前端·python
日月云棠1 小时前
10 Integer —— 最常用的整数包装类深度解析
java·后端
大鸡腿同学1 小时前
大模型为何总 “胡说八道”?做完 RAG 知识库,我看懂了它的底层逻辑
后端