ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于MyBatis插件与注解实现声明式数据脱敏的完整方案

基于MyBatis插件与注解实现声明式数据脱敏的完整方案

1. 项目概述:当数据安全遇上MyBatis的优雅解法

最近在做一个金融后台项目,对接的业务方对数据安全的要求近乎苛刻。每次联调,对方都会反复确认:“这个手机号在日志里能看到全量吗?”“身份证号在数据库里是明文存储的吗?” 这类问题。我们当然做了应用层的加解密,但总有“漏网之鱼”——比如,MyBatis打印的SQL日志,里面可能就躺着完整的敏感数据;再比如,某些内部运营查询页面,开发同学图方便直接用了全量字段的实体类,一不小心就把敏感信息带到了前端。

全表加解密?性能损耗太大,且对模糊查询不友好。在每一个查询方法里手动处理?代码会变得臃肿不堪,且容易遗漏。这时候,我想到了MyBatis的插件机制。它就像潜伏在SQL执行生命周期里的“特工”,我们可以在数据“进”(写入)和“出”(查询)的关键节点进行拦截和改造。结合自定义注解,我们可以用一种非常声明式、无侵入的方式,给需要脱敏的字段打上标记,让插件自动完成脱敏逻辑。这不仅能满足安全审计要求,还能极大地提升开发效率和代码的可维护性。今天,我就来详细拆解一下,如何从零开始打造一个属于自己的MyBatis数据脱敏插件。

2. 核心设计思路与架构拆解

2.1 为什么选择插件+注解的方案?

在决定技术方案前,我们评估过几种常见的脱敏方式。第一种是在Setter/Getter方法里处理,这需要修改所有实体类,侵入性强,且无法覆盖SQL日志场景。第二种是使用AOP拦截Service层方法,这能解决一部分问题,但粒度较粗,且对MyBatis内部执行的SQL结果集处理起来比较麻烦。第三种就是在数据库层面使用视图或函数,但这将业务逻辑下沉到了数据库,不利于维护和扩展。

MyBatis插件(Interceptor)方案的优势在于,它直接作用于MyBatis的核心执行流程。无论是查询结果映射(ResultSetHandler)、参数设置(ParameterHandler)还是SQL语句本身(StatementHandler),我们都能介入。这意味着我们可以:

  1. 在结果集映射时脱敏:拦截查询结果,根据注解将敏感字段内容替换为脱敏后的值(如138****1234)。
  2. 在参数设置时加密:拦截插入/更新操作的参数,根据注解将明文加密后再存入数据库。
  3. 在记录SQL日志时脱敏:拦截执行的SQL语句,将其中包含的敏感参数值替换为占位符或脱敏值,避免日志泄露。

而注解,则是驱动这个插件的“配置元数据”。通过在实体类的字段上添加类似@DataMasking(type = MaskType.PHONE)的注解,我们就清晰地声明了:“这个字段是手机号,需要按手机号规则脱敏”。插件读取这些注解,然后执行相应的逻辑。这种声明式的方式,将脱敏规则与业务代码完全解耦。

2.2 插件执行的生命周期与拦截点选择

MyBatis插件通过实现Interceptor接口,并指定要拦截的方法签名来工作。MyBatis有四大核心对象可以拦截:

  • Executor:执行器,控制整个SQL执行流程,包含update、query、commit等方法。
  • StatementHandler:数据库会话处理器,负责编译SQL、设置参数、执行SQL。
  • ParameterHandler:参数处理器,用于设置预编译SQL语句(PreparedStatement)的参数。
  • ResultSetHandler:结果集处理器,负责将查询结果(ResultSet)映射成Java对象。

对于数据脱敏,我们的主要战场在ResultSetHandlerParameterHandler

  • ResultSetHandler:我们拦截其handleResultSets方法。在这个方法内部,MyBatis已经通过JDBC拿到了数据库返回的原始ResultSet,并正准备将其转换为我们在Mapper中指定的结果对象(无论是单个实体、List还是Map)。此时拦截,我们可以遍历这些即将返回给调用方的对象,根据字段上的注解,修改其值,实现查询结果的自动脱敏。
  • ParameterHandler:我们拦截其setParameters方法。当执行一条INSERT或UPDATE语句时,MyBatis会调用此方法,将Java对象中的属性值设置到SQL的占位符?上。此时拦截,我们可以检查参数对象(通常是实体对象)的字段注解,如果发现需要加密存储的字段,就将其明文值加密成密文,再用密文去设置SQL参数。这样就实现了落库加密。

一个更常见且实用的场景是仅对**出库(查询)**的数据进行脱敏,而入库数据保持明文(或由其他加密服务负责)。这样既能满足前端展示和日志安全的需求,又避免了加解密对数据库查询功能(如模糊查询)的复杂影响。因此,本方案将重点放在ResultSetHandler的拦截上。

2.3 注解的设计:定义脱敏的规则

注解是灵魂,它定义了“对谁”以及“如何”脱敏。一个健壮的脱敏注解至少需要包含以下元素:

/** * 数据脱敏注解 */ @Documented @Target(ElementType.FIELD) // 作用于字段上 @Retention(RetentionPolicy.RUNTIME) // 运行时保留,插件需要通过反射读取 public @interface DataMasking { /** * 脱敏类型 */ MaskType type() default MaskType.CUSTOM; /** * 前置保留长度 (仅对部分类型有效) */ int prefixKeep() default 0; /** * 后置保留长度 (仅对部分类型有效) */ int suffixKeep() default 0; /** * 掩码字符 (仅对CUSTOM或部分类型有效) */ char maskChar() default '*'; /** * 自定义脱敏逻辑的Spring Bean名称 (当type为CUSTOM时使用) */ String customProcessor() default ""; }

同时,我们需要一个枚举MaskType来定义常见的脱敏模式:

public enum MaskType { /** 中文名 */ CHINESE_NAME, /** 身份证号 */ ID_CARD, /** 手机号 */ PHONE, /** 邮箱 */ EMAIL, /** 银行卡号 */ BANK_CARD, /** 自定义 */ CUSTOM; }

这样,在实体类中我们就可以非常清晰地使用:

public class User { private Long id; @DataMasking(type = MaskType.CHINESE_NAME) private String name; @DataMasking(type = MaskType.PHONE) private String mobile; @DataMasking(type = MaskType.ID_CARD, prefixKeep = 6, suffixKeep = 4) private String idCard; // ... 其他字段 }

3. 核心组件实现详解

3.1 脱敏策略工厂与具体策略实现

有了注解定义“规则”,我们需要具体的“工人”来执行脱敏。设计一个策略工厂模式是非常合适的。首先定义一个脱敏策略接口:

public interface MaskingStrategy { /** * 脱敏处理 * @param source 原始字符串 * @param annotation 字段上的@DataMasking注解实例 * @return 脱敏后的字符串 */ String mask(String source, DataMasking annotation); }

然后,为每一种MaskType实现具体的策略。例如,手机号的脱敏策略:

@Component("phoneMaskingStrategy") // 注册为Spring Bean public class PhoneMaskingStrategy implements MaskingStrategy { private static final Pattern PHONE_PATTERN = Pattern.compile("^(\\d{3})\\d{4}(\\d{4})$"); @Override public String mask(String source, DataMasking annotation) { if (source == null || source.length() != 11) { return source; // 非标准手机号,原样返回或按自定义规则处理 } // 将 13800138000 转换为 138****8000 return source.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); } }

再比如,一个通用的保留前后缀的脱敏策略,可以被身份证、银行卡等类型复用:

@Component("commonMaskingStrategy") public class CommonMaskingStrategy implements MaskingStrategy { @Override public String mask(String source, DataMasking annotation) { if (source == null || source.length() == 0) { return source; } int prefixKeep = annotation.prefixKeep(); int suffixKeep = annotation.suffixKeep(); int totalKeep = prefixKeep + suffixKeep; int length = source.length(); // 如果要保留的长度超过或等于原长,返回原值 if (totalKeep >= length) { return source; } // 构建掩码部分 StringBuilder maskedPart = new StringBuilder(); int maskLength = length - totalKeep; for (int i = 0; i < maskLength; i++) { maskedPart.append(annotation.maskChar()); } // 拼接:前段保留 + 掩码 + 后段保留 return source.substring(0, prefixKeep) + maskedPart.toString() + source.substring(length - suffixKeep); } }

最后,创建一个策略工厂,根据注解类型返回对应的策略Bean:

@Component public class MaskingStrategyFactory { @Autowired private Map<String, MaskingStrategy> strategyMap; // Spring会自动将所有MaskingStrategy实现注入为Map,key为Bean name public MaskingStrategy getStrategy(DataMasking annotation) { MaskType type = annotation.type(); String beanName; switch (type) { case PHONE: beanName = "phoneMaskingStrategy"; break; case ID_CARD: case BANK_CARD: beanName = "commonMaskingStrategy"; // 身份证和银行卡使用通用策略 break; case CUSTOM: beanName = annotation.customProcessor(); // 自定义策略,从注解中读取Bean名 break; // ... 其他类型 default: beanName = "commonMaskingStrategy"; } MaskingStrategy strategy = strategyMap.get(beanName); if (strategy == null) { throw new IllegalArgumentException("未找到对应的脱敏策略: " + beanName); } return strategy; } }

实操心得:将策略注册为Spring Bean并通过Map自动装配,比用if-elseswitch直接new对象的方式要优雅和灵活得多。未来新增一种脱敏类型,你只需要新增一个实现MaskingStrategy的Bean即可,工厂类几乎不用修改,符合开闭原则。

3.2 MyBatis插件的核心拦截逻辑

这是插件最核心的部分。我们将实现一个ResultSetHandler的拦截器。

@Intercepts({ @Signature(type = ResultSetHandler.class, // 拦截的目标类 method = "handleResultSets", // 拦截的方法 args = {Statement.class}) // 方法参数类型 }) @Component public class DataMaskingInterceptor implements Interceptor { @Autowired private MaskingStrategyFactory strategyFactory; @Override public Object intercept(Invocation invocation) throws Throwable { // 1. 先执行原方法,获取MyBatis处理后的原始结果 Object result = invocation.proceed(); // 2. 如果结果为空,直接返回 if (result == null) { return null; } // 3. 处理结果集 return processResult(result); } private Object processResult(Object result) { // 3.1 处理结果为单个对象的情况 if (result instanceof List) { List<?> list = (List<?>) result; if (!list.isEmpty()) { // 获取列表第一个元素的Class,假设列表元素类型一致 Class<?> clazz = list.get(0).getClass(); // 只有我们标记了@MaskingData的实体类才进行处理 if (clazz.isAnnotationPresent(MaskingData.class)) { for (Object item : list) { maskObject(item); } } } return list; } else { // 3.2 处理结果为单个对象的情况 Class<?> clazz = result.getClass(); if (clazz.isAnnotationPresent(MaskingData.class)) { maskObject(result); } return result; } } private void maskObject(Object object) { Class<?> clazz = object.getClass(); // 遍历所有字段 for (Field field : clazz.getDeclaredFields()) { // 检查字段是否被@DataMasking注解标记 DataMasking annotation = field.getAnnotation(DataMasking.class); if (annotation != null && field.getType().equals(String.class)) { // 获取策略并执行脱敏 MaskingStrategy strategy = strategyFactory.getStrategy(annotation); try { field.setAccessible(true); // 设置可访问,因为字段通常是private的 String originalValue = (String) field.get(object); if (originalValue != null) { String maskedValue = strategy.mask(originalValue, annotation); field.set(object, maskedValue); // 将脱敏后的值设回字段 } } catch (IllegalAccessException e) { throw new RuntimeException("处理字段[" + field.getName() + "]脱敏时发生反射异常", e); } } } } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // 可以从mybatis配置中读取属性,此处暂不需要 } }

这里引入了一个额外的类级注解@MaskingData。为什么需要它?这是为了性能优化。如果不加这个注解,插件会对所有查询返回的对象进行反射和字段遍历,开销巨大。通过这个注解,我们明确告诉插件:“只有标记了@MaskingData的实体类,才需要检查其字段进行脱敏。” 这相当于一个总开关。

@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface MaskingData { }

然后,在需要脱敏的实体类上加上它:

@MaskingData public class User { // ... 带有@DataMasking注解的字段 }

注意事项:使用反射修改字段值存在一定的性能损耗,并且要求字段不能是final的。在生产环境中,对于海量数据查询,需要评估此开销。一种优化思路是,在插件层面增加一个开关配置,或者通过ThreadLocal传递一个“本次查询是否需要脱敏”的上下文,避免无谓的反射调用。

3.3 插件在Spring Boot中的配置与注册

在Spring Boot项目中,MyBatis插件可以通过自动配置或手动配置的方式注册。最简单的方式是,确保你的插件类(DataMaskingInterceptor``)本身被Spring容器管理(使用了@Component`注解),并且MyBatis的配置类中将其添加到拦截器链。

@Configuration public class MyBatisConfig { @Autowired private DataMaskingInterceptor dataMaskingInterceptor; @Bean public ConfigurationCustomizer mybatisConfigurationCustomizer() { return configuration -> { // 将我们的拦截器添加到MyBatis的拦截器链中 configuration.addInterceptor(dataMaskingInterceptor); }; } }

这样,当应用启动时,我们的脱敏插件就会自动生效。

4. 高级特性与生产级考量

4.1 支持Map、JSON字段与嵌套对象

上面的基础版本处理了简单的实体对象和List。但在实际项目中,结果类型可能更加复杂:

  1. 返回Map<String, Object>:常见于动态查询或统计场景。
  2. 实体类中包含嵌套对象:例如User对象里有一个Account对象,而Account里也有需要脱敏的字段。
  3. 字段值是JSON字符串:数据库中存储的JSON,其中某个属性是手机号,需要在反序列化后脱敏。

对于Map类型,我们可以在processResult方法中增加分支判断。处理逻辑类似,遍历Map的Value,如果Value是String类型且对应的Key符合某种规则(或我们需要维护一个Key-脱敏规则的映射),则进行脱敏。但更通用的做法是,将Map也视为一个“对象”,为其定义一个虚拟的“类级注解”映射。

对于嵌套对象,我们需要递归地处理。修改maskObject方法:

private void maskObject(Object object) { if (object == null) { return; } Class<?> clazz = object.getClass(); // 只有标记了@MaskingData的类,才递归处理其字段 if (!clazz.isAnnotationPresent(MaskingData.class)) { return; } for (Field field : clazz.getDeclaredFields()) { field.setAccessible(true); try { Object fieldValue = field.get(object); if (fieldValue == null) { continue; } // 情况1:字段是String类型且有@DataMasking注解 DataMasking annotation = field.getAnnotation(DataMasking.class); if (annotation != null && fieldValue instanceof String) { MaskingStrategy strategy = strategyFactory.getStrategy(annotation); String maskedValue = strategy.mask((String) fieldValue, annotation); field.set(object, maskedValue); } // 情况2:字段是对象或集合,递归处理 else if (fieldValue instanceof Collection) { for (Object item : (Collection<?>) fieldValue) { maskObject(item); // 递归处理集合中的每个元素 } } else { // 递归处理嵌套对象 maskObject(fieldValue); } } catch (IllegalAccessException e) { // 记录日志,继续处理其他字段 log.warn("处理字段[{}]时发生反射异常", field.getName(), e); } } }

踩坑提醒:递归处理需要特别注意循环引用的问题。例如,User对象里有一个List<Order>,而Order对象里又引用了User对象。不加控制地递归会导致栈溢出。解决方案是使用一个ThreadLocal<Set<Object>>来记录当前调用链中已经处理过的对象引用,如果再次遇到则跳过。

对于JSON字段,处理起来更复杂。一种思路是,在脱敏插件之后,再增加一个Jackson的序列化/反序列化定制器。例如,定义一个Jackson的JsonSerializer,专门用于处理带有@DataMasking注解的字段。这样,无论数据来自数据库还是其他来源,在序列化成JSON响应给前端时,都会自动脱敏。这需要将脱敏逻辑从MyBatis插件中部分抽离,形成更通用的工具。

4.2 性能优化与开关控制

全量反射对性能的影响不容忽视。除了使用@MaskingData进行粗粒度过滤,还可以进行以下优化:

  1. 缓存反射信息:每次处理一个对象类型时,都通过clazz.getDeclaredFields()和遍历查找注解,开销很大。我们可以使用一个ConcurrentHashMap<Class<?>, List<MaskingFieldInfo>>来缓存每个需要脱敏的类及其需要处理的字段信息(字段对象、注解、对应的策略)。这样,每个类只在第一次被处理时进行反射解析。

    private static class MaskingFieldInfo { final Field field; final DataMasking annotation; final MaskingStrategy strategy; // 甚至可以缓存策略实例 MaskingFieldInfo(Field field, DataMasking annotation, MaskingStrategy strategy) { this.field = field; this.annotation = annotation; this.strategy = strategy; this.field.setAccessible(true); // 在这里提前设置accessible } }
  2. 全局/请求级开关:在某些内部接口或导出数据时,我们可能需要获取原始数据。可以通过以下方式实现开关:

    • 基于注解:在Mapper方法上添加一个注解,如@SkipMasking,插件在执行时,通过Invocation对象获取到当前执行的Mapper方法,检查该方法是否有此注解,有则跳过脱敏。
    • 基于ThreadLocal:在业务代码中,通过一个工具类设置ThreadLocal<Boolean>,插件在执行时检查这个上下文变量。例如:
      public class MaskingContext { private static final ThreadLocal<Boolean> SKIP_MASKING = ThreadLocal.withInitial(() -> false); public static void skipMaskingForCurrentThread() { SKIP_MASKING.set(true); } public static void resetMaskingForCurrentThread() { SKIP_MASKING.remove(); } public static boolean isSkipMasking() { return SKIP_MASKING.get(); } }
      在插件intercept方法开头检查if (MaskingContext.isSkipMasking()) { return invocation.proceed(); }务必注意:一定要在finally块中或在后续的过滤器中清理ThreadLocal,否则会导致内存泄漏和状态污染。

4.3 与MyBatis-Plus等框架的兼容性

如果你的项目使用了MyBatis-Plus(MP),需要注意插件的加载顺序和MP自身插件(如分页插件PaginationInnerInterceptor)的兼容性。MP的插件也是通过MyBatis的拦截器机制实现的。

在配置时,需要确保插件被添加到InterceptorChain的顺序符合预期。通常,数据脱敏插件应该在结果集处理的最外层,即等其他所有插件(如分页插件)都处理完后,再对最终的结果进行脱敏。在Spring Boot中,通过@Order注解或显式地在ConfigurationCustomizer中控制添加顺序可能不直观。更可靠的方式是在MP的配置类中,直接组装拦截器链:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor(DataMaskingInterceptor dataMaskingInterceptor) { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 1. 先添加其他MP插件,比如分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // ... 其他MP插件 // 2. 注意:MP的Interceptor是一个包装器,我们的插件需要单独添加 // 通常我们需要获取SqlSessionFactory,然后通过其Configuration添加 // 但这里更推荐在下面的方法中配置 return interceptor; } @Bean public ConfigurationCustomizer configurationCustomizer(DataMaskingInterceptor dataMaskingInterceptor) { return configuration -> { // 确保在MP插件之后添加我们的脱敏插件 // 因为脱敏作用于最终结果,顺序靠后更安全 configuration.addInterceptor(dataMaskingInterceptor); }; } }

重要提示:务必进行充分的集成测试。编写一个单元测试,模拟MP分页查询,断言返回的分页对象Page中的记录数据是否被正确脱敏。有时MP会对结果进行包装,你需要定位到真正的数据列表所在的位置。

5. 测试、部署与问题排查

5.1 编写单元测试与集成测试

一个健壮的组件离不开测试。我们需要为脱敏策略和插件核心逻辑编写单元测试,并为整个数据访问层编写集成测试。

策略单元测试示例:

@SpringBootTest class PhoneMaskingStrategyTest { @Autowired private PhoneMaskingStrategy strategy; @Test void testMask_StandardPhone() { DataMasking annotation = mock(DataMasking.class); when(annotation.type()).thenReturn(MaskType.PHONE); String result = strategy.mask("13800138000", annotation); assertEquals("138****8000", result); } @Test void testMask_InvalidPhone() { DataMasking annotation = mock(DataMasking.class); when(annotation.type()).thenReturn(MaskType.PHONE); // 测试非11位手机号 assertEquals("12345", strategy.mask("12345", annotation)); // 测试null assertNull(strategy.mask(null, annotation)); } }

插件集成测试示例(使用H2内存数据库):

@SpringBootTest @Transactional // 测试后回滚数据 class DataMaskingInterceptorTest { @Autowired private UserMapper userMapper; @Test void testQueryWithMasking() { // 1. 插入一条原始数据 User user = new User(); user.setName("张三"); user.setMobile("13800138000"); user.setIdCard("110101199001011234"); userMapper.insert(user); // 2. 查询该数据 User queriedUser = userMapper.selectById(user.getId()); // 3. 断言脱敏生效 assertNotNull(queriedUser); assertEquals("138****8000", queriedUser.getMobile()); // 手机号脱敏 assertEquals("110101********1234", queriedUser.getIdCard()); // 身份证保留前6后4 assertEquals("张三", queriedUser.getName()); // 姓名未加注解,应保持不变 } @Test void testQueryListWithMasking() { // 插入多条数据... List<User> userList = userMapper.selectList(null); assertFalse(userList.isEmpty()); for (User u : userList) { assertTrue(u.getMobile().contains("****")); // 所有手机号都应被脱敏 } } }

5.2 常见问题排查清单

在实际使用中,你可能会遇到以下问题:

问题现象可能原因排查步骤与解决方案
脱敏完全没生效1. 插件未注册成功。
2. 实体类未加@MaskingData注解。
3. 字段类型不是String
1. 检查Spring Boot启动日志,确认DataMaskingInterceptorBean已创建,并查看MyBatis配置日志。
2. 确认查询返回的实体类上有@MaskingData注解。
3. 确认被@DataMasking注解标记的字段是String类型。
部分字段脱敏,部分未脱敏1. 字段名拼写错误或注解放置位置不对(应放在字段上,而非getter方法)。
2. 使用了Lombok等框架,编译后注解位置可能变化。
1. 使用IDE或反射API检查运行时字段上的注解信息。
2. 如果使用Lombok,确保@DataMasking注解在生成的Getter/Setter上依然有效,可能需要使用@Getter@Setter在字段上,或将注解放在正确的Getter方法上(如果插件设计为检查方法注解)。建议本插件统一针对字段设计
脱敏后数据变null脱敏策略中对null或空字符串处理不当。检查你的MaskingStrategy实现,确保在输入为null或空字符串时,直接原样返回,而不是返回null或抛出异常。
性能明显下降1. 未使用@MaskingData过滤,导致所有查询对象都被反射扫描。
2. 嵌套对象层次太深,递归处理开销大。
3. 缓存未生效。
1. 确保所有需要脱敏的实体类都标记了@MaskingData
2. 评估是否真的需要深度递归脱敏,或许可以调整数据结构。
3. 检查反射信息缓存逻辑是否正确,确保同一种类只解析一次。
与MyBatis-Plus分页插件冲突插件执行顺序问题,脱敏可能发生在分页插件计算总数之前,导致分页数据错乱。调整插件注册顺序,确保脱敏插件在MP分页插件之后执行。可以通过调试,查看InterceptorChain中插件的顺序,或在MP配置中显式控制。最稳妥的方式是,脱敏插件只处理ResultSetHandler,而MP分页插件主要作用于Executor,两者通常不冲突,但需测试验证。
日志中SQL参数仍显示明文插件只拦截了ResultSetHandler,未拦截ParameterHandler或SQL语句日志本身。如果需要对日志中的参数进行脱敏,需要额外拦截ParameterHandlersetParameters方法,或者使用一个专门的StatementHandler插件来重写SQL日志输出逻辑。这是一个更高级的需求,实现时要注意性能。

5.3 生产环境部署建议

  1. 灰度发布:首次上线时,可以先在非核心业务或查询量较小的服务上启用,观察日志和监控,确认无性能问题和功能异常后,再全量推广。
  2. 监控与告警:在插件的关键方法(如intercept)入口处增加监控点,记录处理耗时、处理的对象数量。如果平均耗时超过阈值(如5ms),触发告警,以便及时优化。
  3. 配置化:将脱敏规则(如手机号保留前3后4)提取到配置中心(如Apollo、Nacos),而不是硬编码在策略类中。这样可以在不重启服务的情况下,动态调整脱敏规则。
  4. 文档与规范:在团队内部明确脱敏注解的使用规范。例如,规定所有DTO(Data Transfer Object)中,只要包含敏感信息字段,就必须使用对应的脱敏注解。可以将此作为代码审查(Code Review)的一项必查项。

这个自制的MyBatis数据脱敏插件,从最初的简单想法,到考虑各种边界情况和生产优化,是一个不断打磨的过程。它不仅仅是一个工具,更是一种将安全诉求无缝融入开发流程的实践。看到团队成员在新建实体类时,能自然而然地加上@DataMasking注解,而不是到处写substringreplace,就知道这个轮子造得值了。

返回列表