news 2026/3/12 4:00:04

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot动态脱敏实战,从注解到AOP的优雅打码术

大家好,我是小悟。

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

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

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

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

二、SpringBoot脱敏方案实战

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

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

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:实现脱敏序列化器,专业的“打码师”

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,告诉它:“看这里,要打码!”

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:在实体类上使用,贴上标签就自动打码

/** * 用户实体:敏感字段都穿上了“马赛克小内裤” */ @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:控制器测试一下效果

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

测试结果:

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

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

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

/** * 脱敏策略:定义各种脱敏算法 * 就像不同的美颜滤镜 */ 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切面实现

@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拦截器方案(数据库查询时的美颜相机)

/** * 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出口处的安检机)

/** * 自定义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:数据库层脱敏方案(给数据库戴上口罩)

/** * 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.分层防御:不要把所有鸡蛋放在一个篮子里

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

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

@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.性能优化:脱敏也要注意效率

@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.监控与日志:知道谁在什么时候脱敏

@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. 性能平衡:在安全和性能之间找到最佳平衡点

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

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

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

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

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

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

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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/11 2:26:56

震惊!选对酶制剂,这3家必须知道!

震惊&#xff01;选对酶制剂&#xff0c;这3家必须知道&#xff01;在当今的生物制造、食品加工、饲料工业乃至环保清洁等多个领域&#xff0c;酶制剂作为高效的生物催化剂&#xff0c;其重要性日益凸显。面对市场上品牌林立、产品繁多的局面&#xff0c;如何精准选择技术领先、…

作者头像 李华
网站建设 2026/3/10 3:46:57

Vectras VM Android虚拟机完整教程:手机变身全能桌面工作站

Vectras VM Android虚拟机完整教程&#xff1a;手机变身全能桌面工作站 【免费下载链接】Vectras-VM-Android Its a Virtual Machine App for Android Which is Based on QEMU 项目地址: https://gitcode.com/gh_mirrors/ve/Vectras-VM-Android 还在为无法在移动设备上体…

作者头像 李华
网站建设 2026/3/11 10:49:17

物理信息神经网络终极指南:从零基础到实战应用

物理信息神经网络终极指南&#xff1a;从零基础到实战应用 【免费下载链接】PINNs Physics Informed Deep Learning: Data-driven Solutions and Discovery of Nonlinear Partial Differential Equations 项目地址: https://gitcode.com/gh_mirrors/pi/PINNs 物理信息神…

作者头像 李华
网站建设 2026/3/11 2:49:29

TDesign Vue Next 终极开发指南:从零构建现代化Vue 3应用

TDesign Vue Next 终极开发指南&#xff1a;从零构建现代化Vue 3应用 【免费下载链接】tdesign-vue-next A Vue3.x UI components lib for TDesign. 项目地址: https://gitcode.com/gh_mirrors/tde/tdesign-vue-next 还在为Vue 3项目寻找一套完美的UI组件库吗&#xff1…

作者头像 李华