Spring Boot数据脱敏实战:Jasypt加密配置与MyBatis-Plus插件实现
1. 项目概述为什么数据脱敏是后端开发的必修课最近在做一个用户信息管理的后台系统上线前安全团队做了一轮渗透测试结果让人后背发凉。测试人员仅仅通过一个普通的列表查询接口就拿到了数据库里所有用户的真实手机号和身份证号。问题出在哪不是SQL注入也不是越权而是我们图省事在返回给前端的JSON里直接把数据库查出来的明文敏感字段原样吐了出去。日志里、监控里、甚至某些调试接口的缓存里这些数据都可能被记录下来。这件事给我敲了警钟数据安全尤其是敏感数据的保护绝不能只依赖数据库的访问权限必须在应用层就建立起坚固的防线。这就是“数据脱敏”要解决的核心问题。所谓数据脱敏就是在确保业务功能正常的前提下对敏感数据进行变形、替换或加密使其在非生产环境如开发、测试、日志、监控或对非授权人员如普通运营、前端展示不可见其真实内容。它不同于全程加密如HTTPS后者解决的是传输过程中的窃听问题脱敏关注的是数据在“静止”和“使用”状态下的暴露风险。一个典型的场景是客服人员需要查看用户订单列表以处理客诉他需要看到用户的姓氏和部分手机号如张* 138****1234来确认身份但绝不应该看到完整的身份证号。基于这个需求我决定在当前的Spring Boot项目中构建一套落地性强、对代码侵入性低的数据脱敏方案。经过一番调研和对比最终选定了Jasypt负责配置文件的加密以及MyBatis-Plus的字段脱敏插件来处理业务数据。这套组合拳能很好地覆盖从“配置保密”到“数据展示”的全链路安全场景。接下来我就把这套方案的详细设计思路、踩坑实录和完整实现过程分享出来。2. 技术选型与架构设计为什么是Jasypt MyBatis-Plus在动手之前我先梳理了项目中需要保护的敏感数据主要分两类静态配置敏感信息 主要是application.yml里的数据库密码、Redis密码、第三方API的密钥等。这些信息如果以明文写在配置文件里一旦代码仓库泄露后果不堪设想。动态业务敏感数据 即业务运行时产生的用户隐私数据如手机号、邮箱、身份证号、银行卡号等。它们存在于数据库并在查询后通过Controller返回给前端或记录到日志。针对这两类数据我评估了几种常见方案数据库层面加密 使用MySQL的AES_ENCRYPT等函数。优点是加解密在数据库完成应用无感知。缺点是严重绑定数据库厂商且密文数据无法利用索引查询性能会急剧下降同时数据库管理员DBA依然能看到明文。在Service层手动处理 在每一个查询方法里对返回的DTO对象进行遍历和脱敏。这种方式控制力最强但代码重复度高侵入性强极易遗漏维护成本巨大。利用序列化器如Jackson 在字段的Getter方法或使用JsonSerialize注解指定自定义序列化器。这能很好地解决接口返回时的脱敏但无法解决日志打印、或其他非JSON输出场景如导出到Excel的数据泄露。在持久层拦截处理 在数据从数据库取出后、放入Java实体对象之前或从实体对象写入数据库之前进行拦截。这正是MyBatis插件Interceptor的用武之地。我的设计目标是配置安全无明文数据脱敏自动化代码侵入最小化。基于此最终架构如下Jasypt for 配置加密 负责解决第一类问题。它是一个成熟的Java加密库能轻松集成到Spring Boot对配置文件中的特定值如spring.datasource.password进行加密。应用启动时Jasypt会自动解密这些值并注入到Spring环境中。这样提交到Git的配置文件中敏感信息就是一段不可读的密文。MyBatis-Plus 字段脱敏插件 for 业务数据 负责解决第二类问题。MyBatis-Plus是MyBatis的增强工具其提供的InnerInterceptor接口允许我们在SQL执行的生命周期中进行拦截。我计划编写一个插件在查询结果映射到实体对象ResultSetHandler阶段后对实体中标记了特定注解的字段进行脱敏处理。这样无论这个实体对象之后是被Controller返回、被日志打印、还是被其他服务方法使用它携带的都已经是脱敏后的数据从根源上避免了泄露。这个架构的优势在于关注点分离且互补。Jasypt守护了应用的“入口”安全配置而MyBatis-Plus插件则守护了业务的“核心”安全数据。两者结合基本实现了敏感数据在应用内的“零明文”流转。注意 这里必须明确一个关键概念我们讨论的脱敏主要是“展示脱敏”即数据在数据库里依然是明文存储或可逆加密存储。这是因为业务可能需要根据手机号前三位进行分库分表或者需要根据身份证号计算年龄。如果需要在数据库层存储密文则需要设计更复杂的方案如使用盲索引等这不在本文重点讨论范围内。3. 核心模块一使用Jasypt加密应用配置文件首先处理最危险的“明文密码写死在配置文件里”的问题。Jasypt的集成非常顺畅。3.1 引入依赖与基础配置在项目的pom.xml中添加依赖。这里我选择的是与Spring Boot集成度更高的jasypt-spring-boot-starter。dependency groupIdcom.github.ulisesbocchio/groupId artifactIdjasypt-spring-boot-starter/artifactId version3.0.5/version !-- 请使用最新稳定版本 -- /dependency接下来需要生成一个加密密钥。这个密钥至关重要它是解密配置文件中密文的唯一凭证必须妥善保管如放在服务器的环境变量中而非代码里。我使用Jasypt提供的命令行工具来生成一个加密后的密码并用它来加密我的数据库真实密码。# 假设我们想用密钥‘MySecretKey’来加密 java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI inputMyRealDbPassword passwordMySecretKey algorithmPBEWithMD5AndDES执行后会输出类似ENC(auN8K2ZqBz5VpBzRX5r4HQ)的结果。其中auN8K2ZqBz5VpBzRX5r4HQ就是加密后的密文ENC()是Jasypt识别需要解密的包裹前缀。3.2 改造application.yml配置文件有了密文就可以改造配置文件了。将原来的明文密码替换为ENC(密文)格式。# application.yml spring: datasource: url: jdbc:mysql://localhost:3306/my_db?useSSLfalseserverTimezoneUTC username: root password: ENC(auN8K2ZqBz5VpBzRX5r4HQ) # 这里替换为Jasypt加密后的密文 driver-class-name: com.mysql.cj.jdbc.Driver # Jasypt配置 jasypt: encryptor: # 指定解密算法与加密时保持一致 algorithm: PBEWithMD5AndDES # 这个password就是上面命令行里的‘MySecretKey’。重要绝不能写死在这里 # 正确做法是通过环境变量或启动参数传入例如-Djasypt.encryptor.passwordMySecretKey # password: MySecretKey # 错误示例切勿提交此配置到仓库。 # 我们这里先注释掉通过其他方式注入3.3 安全地传递解密密钥将解密密钥MySecretKey写在配置文件里等于没加密。正确的做法是通过系统环境变量或Java启动参数传递。方式一启动参数推荐用于本地开发java -jar your-application.jar --jasypt.encryptor.passwordMySecretKey方式二系统环境变量在服务器上设置环境变量JASYPT_ENCRYPTOR_PASSWORDMySecretKey然后在配置文件中引用jasypt: encryptor: algorithm: PBEWithMD5AndDES password: ${JASYPT_ENCRYPTOR_PASSWORD} # 从环境变量读取方式三使用更安全的密钥管理服务在生产环境中更安全的做法是使用云服务商提供的密钥管理服务如AWS KMS, Azure Key Vault, 阿里云KMS来存储和获取密钥在应用启动时动态获取。实操心得 在团队协作中我建议在项目README或内部Wiki中维护一个config-template.yml文件里面包含所有需要的配置项结构但敏感值用ENC(***)或${ENV_VAR}占位。真实的、包含密钥的配置通过CI/CD流水线在部署时注入或者由运维人员直接管理在生产服务器上。这样就从流程上杜绝了密钥泄露。完成以上步骤后启动Spring Boot应用Jasypt会自动识别ENC()包裹的配置项并用提供的密钥解密将明文值注入到Spring环境中。你的数据库连接就能正常建立了而你的代码仓库里再也不会有明文密码。4. 核心模块二设计与实现MyBatis-Plus数据脱敏插件解决了配置安全问题接下来是重头戏业务数据的自动化脱敏。我们的目标是实现一个注解比如DataMasking标记在实体类的字段上。当MyBatis-Plus从数据库查询出数据后插件自动对这些字段进行脱敏处理。4.1 定义脱敏注解与策略枚举首先定义一个注解用来标记需要脱敏的字段并指定脱敏策略。import java.lang.annotation.*; /** * 数据脱敏注解 */ Documented Target(ElementType.FIELD) // 只能用在字段上 Retention(RetentionPolicy.RUNTIME) // 运行时保留以便插件读取 public interface DataMasking { /** * 脱敏策略 */ MaskingStrategy strategy(); }然后定义一个脱敏策略枚举包含几种常见的脱敏规则/** * 脱敏策略枚举 */ public enum MaskingStrategy { /** * 中文名只显示第一个字其余隐藏为星号如张** */ CHINESE_NAME, /** * 身份证号显示前六位和后四位如110105******1234 */ ID_CARD, /** * 手机号显示前三位和后四位如138****1234 */ PHONE, /** * 邮箱符号前的第一个字符和域名显示中间用***代替如z***gmail.com */ EMAIL, /** * 银行卡号显示前六位和后四位如622848******1234 */ BANK_CARD, /** * 自定义需配合注解的customPattern参数使用后续扩展 */ CUSTOM }4.2 实现脱敏工具类接下来实现一个工具类根据不同的策略执行具体的脱敏算法。import org.apache.commons.lang3.StringUtils; import java.util.regex.Pattern; public class DataMaskingUtil { // 身份证号正则简易版 private static final Pattern ID_CARD_PATTERN Pattern.compile(^(\\d{6})\\d*(\\d{4})$); // 手机号正则国内 private static final Pattern PHONE_PATTERN Pattern.compile(^(\\d{3})\\d*(\\d{4})$); // 银行卡号正则简易版 private static final Pattern BANK_CARD_PATTERN Pattern.compile(^(\\d{6})\\d*(\\d{4})$); // 邮箱正则 private static final Pattern EMAIL_PATTERN Pattern.compile(^(.)(.*)(.*)$); /** * 执行脱敏 * param origin 原始字符串 * param strategy 脱敏策略 * return 脱敏后的字符串若原始字符串为空或不符合规则返回原值或空 */ public static String mask(String origin, MaskingStrategy strategy) { if (StringUtils.isBlank(origin)) { return origin; } switch (strategy) { case CHINESE_NAME: return maskChineseName(origin); case ID_CARD: return maskPattern(origin, ID_CARD_PATTERN); case PHONE: return maskPattern(origin, PHONE_PATTERN); case EMAIL: return maskEmail(origin); case BANK_CARD: return maskPattern(origin, BANK_CARD_PATTERN); case CUSTOM: // 自定义逻辑可扩展 return origin; default: return origin; } } private static String maskChineseName(String name) { if (name.length() 1) { return name; } return name.charAt(0) StringUtils.repeat(*, name.length() - 1); } private static String maskPattern(String origin, Pattern pattern) { java.util.regex.Matcher matcher pattern.matcher(origin); if (matcher.matches()) { String prefix matcher.group(1); String suffix matcher.group(2); int maskLength origin.length() - prefix.length() - suffix.length(); return prefix StringUtils.repeat(*, maskLength) suffix; } // 如果不匹配正则进行简单脱敏保留前3后4 return simpleMask(origin, 3, 4); } private static String maskEmail(String email) { java.util.regex.Matcher matcher EMAIL_PATTERN.matcher(email); if (matcher.matches()) { String firstChar matcher.group(1); String domain matcher.group(3); // 包含符号 return firstChar *** domain; } return email; } private static String simpleMask(String origin, int preLen, int sufLen) { if (origin.length() preLen sufLen) { return StringUtils.repeat(*, origin.length()); } String prefix StringUtils.left(origin, preLen); String suffix StringUtils.right(origin, sufLen); int maskLength origin.length() - preLen - sufLen; return prefix StringUtils.repeat(*, maskLength) suffix; } }4.3 实现MyBatis-Plus脱敏拦截器插件这是最核心的部分。我们需要实现MyBatis-Plus的InnerInterceptor接口并重写beforeQuery或afterQuery方法。我选择在afterQuery阶段即SQL执行完毕结果集准备映射到Java对象时进行处理。import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper; import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.executor.resultset.ResultSetHandler; import org.apache.ibatis.plugin.*; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.util.ReflectionUtils; import java.lang.reflect.Field; import java.sql.Statement; import java.util.*; Intercepts({ Signature(type ResultSetHandler.class, method handleResultSets, args {Statement.class}) }) Slf4j public class DataMaskingInterceptor implements InnerInterceptor { Override public void beforeQuery(MybatisPlusInterceptor interceptor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { // 可以在查询前做一些事情这里我们主要处理结果所以此方法空实现或调用父类 InnerInterceptor.super.beforeQuery(interceptor, ms, parameter, rowBounds, resultHandler, boundSql); } /** * 核心方法在结果集处理完成后对包含DataMasking注解的字段进行脱敏 */ Override public void afterQuery(MybatisPlusInterceptor interceptor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql, ListObject resultList) throws SQLException { // 如果结果集为空直接返回 if (CollectionUtils.isEmpty(resultList)) { return; } // 遍历结果集中的每一个对象 for (Object result : resultList) { if (result null) { continue; } // 处理单个结果对象 processResultObject(result); } } private void processResultObject(Object obj) { Class? clazz obj.getClass(); // 获取该类所有声明的字段 Field[] fields clazz.getDeclaredFields(); for (Field field : fields) { // 检查字段是否被DataMasking注解标记 DataMasking annotation AnnotationUtils.findAnnotation(field, DataMasking.class); if (annotation ! null) { // 设置字段可访问针对private字段 field.setAccessible(true); try { Object originalValue field.get(obj); if (originalValue instanceof String) { String maskedValue DataMaskingUtil.mask((String) originalValue, annotation.strategy()); // 将脱敏后的值设置回字段 field.set(obj, maskedValue); if (log.isDebugEnabled()) { log.debug(字段 [{}] 脱敏完成: {} - {}, field.getName(), originalValue, maskedValue); } } } catch (IllegalAccessException e) { log.error(处理脱敏字段 [{}] 时发生异常, field.getName(), e); } finally { // 恢复字段的访问权限 field.setAccessible(false); } } } // 递归处理对象中的嵌套对象如果存在例如一对一关联 // 这里可以根据需要扩展例如处理具有OneToOne注解的字段 } }4.4 配置插件到Spring Boot最后将我们自定义的拦截器配置到MyBatis-Plus的插件链中。import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 添加数据脱敏插件 interceptor.addInnerInterceptor(new DataMaskingInterceptor()); // 你可以继续添加其他插件比如分页插件 // interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; } }4.5 在实体类上应用脱敏注解现在就可以在实体类中使用了。假设有一个User实体import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; Data TableName(sys_user) public class User { private Long id; private String username; DataMasking(strategy MaskingStrategy.CHINESE_NAME) private String realName; DataMasking(strategy MaskingStrategy.PHONE) private String phone; DataMasking(strategy MaskingStrategy.EMAIL) private String email; DataMasking(strategy MaskingStrategy.ID_CARD) private String idCard; // ... 其他字段 }当执行userMapper.selectList(queryWrapper)时查询返回的ListUser中realName,phone,email,idCard字段都已经是脱敏后的数据。无论这个列表是被直接返回给前端还是被记录到日志文件看到的都将是“张**”、“1381234”、“zexample.com”、“110105*****1234”这样的信息。5. 进阶优化与生产级考量基础的脱敏插件跑通了但在实际生产环境中我们还需要考虑更多边界情况和性能问题。5.1 处理嵌套对象与集合上面的插件只处理了当前对象的直接字段。如果实体类中包含嵌套对象如private UserDetail detail;或者集合如private ListBankCard cards;并且这些嵌套对象里也有需要脱敏的字段我们的插件就无法处理了。我们需要修改processResultObject方法增加递归处理逻辑。private void processResultObject(Object obj) { if (obj null) { return; } Class? clazz obj.getClass(); // 处理基本类型、包装类型、字符串等直接返回 if (clazz.isPrimitive() || clazz.isEnum() || CharSequence.class.isAssignableFrom(clazz) || Number.class.isAssignableFrom(clazz) || Date.class.isAssignableFrom(clazz)) { return; } // 处理集合类型List, Set if (Collection.class.isAssignableFrom(clazz)) { Collection? collection (Collection?) obj; for (Object item : collection) { processResultObject(item); } return; } // 处理Map类型 if (Map.class.isAssignableFrom(clazz)) { Map?, ? map (Map?, ?) obj; for (Map.Entry?, ? entry : map.entrySet()) { processResultObject(entry.getValue()); } return; } // 处理数组 if (clazz.isArray()) { Object[] array (Object[]) obj; for (Object item : array) { processResultObject(item); } return; } // 处理当前对象的直接字段原有的脱敏逻辑 Field[] fields clazz.getDeclaredFields(); for (Field field : fields) { // 先处理字段本身的脱敏注解 DataMasking annotation AnnotationUtils.findAnnotation(field, DataMasking.class); if (annotation ! null field.getType().equals(String.class)) { // ... 脱敏逻辑同上 } // 然后递归处理字段值如果它是对象、集合等 try { field.setAccessible(true); Object fieldValue field.get(obj); if (fieldValue ! null !field.getType().isPrimitive()) { // 递归调用处理嵌套结构 processResultObject(fieldValue); } } catch (IllegalAccessException e) { log.warn(无法访问字段 {} 进行递归脱敏处理, field.getName(), e); } finally { field.setAccessible(false); } } }这样修改后插件就能深度遍历整个结果对象图确保所有嵌套层次的敏感字段都被脱敏。5.2 性能影响分析与优化反射操作是有性能开销的。每次查询都要遍历结果集中每个对象的每个字段进行注解检查和反射设值在数据量很大时比如万级以上列表查询可能会成为瓶颈。我们可以从以下几个方向优化缓存注解信息 避免每次处理对象时都用反射获取字段和注解。可以在插件初始化时扫描常用的实体类将Class, ListFieldMaskingMeta的映射关系缓存起来。FieldMaskingMeta可以包含字段对象和对应的脱敏策略。private static final ConcurrentHashMapClass?, ListFieldMaskingMeta CLASS_FIELD_CACHE new ConcurrentHashMap(); private ListFieldMaskingMeta getMaskingFields(Class? clazz) { return CLASS_FIELD_CACHE.computeIfAbsent(clazz, key - { ListFieldMaskingMeta list new ArrayList(); Field[] fields clazz.getDeclaredFields(); for (Field field : fields) { DataMasking annotation AnnotationUtils.findAnnotation(field, DataMasking.class); if (annotation ! null field.getType().equals(String.class)) { field.setAccessible(true); // 在这里就设置可访问并缓存起来 list.add(new FieldMaskingMeta(field, annotation.strategy())); } } return list; }); }这样在处理对象时直接从缓存中获取需要脱敏的字段列表省去了反射查找注解的开销。支持忽略特定查询 不是所有查询都需要脱敏。例如后台管理员的导出功能或某些内部计算可能需要原始数据。我们可以设计一个忽略脱敏的机制。一种简单的方式是使用MyBatis-Plus的InterceptorIgnore注解如果其功能满足或者自定义一个上下文持有器如ThreadLocal在需要忽略脱敏的Service方法开始时设置一个标志在插件中检查这个标志。public class DataMaskingContext { private static final ThreadLocalBoolean IGNORE_MASKING ThreadLocal.withInitial(() - false); public static void setIgnoreMasking(boolean ignore) { IGNORE_MASKING.set(ignore); } public static boolean isIgnoreMasking() { return IGNORE_MASKING.get(); } public static void clear() { IGNORE_MASKING.remove(); } } // 在插件中 if (DataMaskingContext.isIgnoreMasking()) { return; } // 在Service中 Transactional public ListUser exportUsers() { try { DataMaskingContext.setIgnoreMasking(true); return userMapper.selectList(new QueryWrapper()); // 这次查询结果不脱敏 } finally { DataMaskingContext.clear(); // 务必清理防止内存泄漏和影响其他线程 } }选择性拦截 可以通过分析MappedStatement的ID只对特定的Mapper方法如select*,get*进行脱敏拦截而对于update*,insert*,delete*等则跳过进一步提升效率。5.3 与Jackson序列化脱敏的对比与结合如前所述在Getter上使用JsonSerialize或实现自定义Jackson序列化器也能实现接口返回脱敏。那么两种方案如何选择MyBatis-Plus插件方案优点 脱敏发生在数据从数据库取出的瞬间此后在Java内存中流转的实体对象始终是脱敏状态。这能从根本上防止数据在任何地方日志、监控、调试、甚至是被其他未经验证的方法调用泄露安全性更高。缺点 一旦脱敏在同一个事务或线程内如果需要使用原始数据比如在同一个Service方法里先查出来脱敏展示又要用原手机号发短信就会有问题。需要通过DataMaskingContext忽略脱敏或者查询两次一次脱敏用于展示一次不脱敏用于业务。Jackson序列化方案优点 只影响最终HTTP响应体的JSON输出不影响Java内存中的对象状态。业务逻辑中使用的始终是原始数据更灵活。缺点 如果实体对象被用于日志打印如log.info(“user: {}”, user)或者被其他非JSON序列化的方式输出如Thymeleaf模板渲染、导出CSV敏感数据依然会泄露。我的建议是将MyBatis-Plus插件作为默认的、全局的数据安全底线确保从数据库取出的数据在应用内“默认安全”。对于少数确实需要在业务逻辑中使用原始数据的场景使用DataMaskingContext进行精细化的临时关闭。这样在安全性和灵活性之间取得了较好的平衡。绝对不要依赖Jackson序列化作为唯一的数据脱敏手段。6. 常见问题排查与实战心得在实际落地过程中我遇到了不少问题这里总结一下希望能帮你避坑。6.1 Jasypt相关报错与解决问题1启动报错Failed to bind properties under ‘spring.datasource.password‘ to java.lang.String现象 应用启动失败提示配置绑定错误。原因 最常见的原因是解密失败。要么是密文格式不对没加ENC()包裹要么是解密密钥jasypt.encryptor.password没有正确传递给应用。排查检查application.yml中密文是否正确包裹在ENC()内。检查启动命令或环境变量是否设置了正确的密钥。可以在启动命令中增加-Djasypt.encryptor.passwordYourKey试试。检查加密时使用的算法algorithm和初始化向量iv-generator-classname如果用了AES等算法是否与配置一致。Jasypt 3.x版本默认使用PBEWITHHMACSHA512ANDAES_256比旧版的PBEWithMD5AndDES更安全但需要Java 8且安装JCE无限强度权限策略文件。如果环境受限可以显式指定为旧算法PBEWithMD5AndDES。问题2集成Nacos等配置中心时加密属性不生效现象 配置从Nacos读取但ENC(...)没有被解密。原因 Jasypt的解密发生在Spring Boot属性加载的早期。如果Nacos客户端在Jasypt解密器初始化之后才加载配置那么这些配置就不会被处理。解决 确保Jasypt Bean的初始化顺序早于Nacos属性源加载。可以尝试在bootstrap.yml中配置Jasypt并使用EnableEncryptableProperties注解。或者考虑使用配置中心本身提供的加密功能如Nacos的cipher-前缀。6.2 MyBatis-Plus脱敏插件不生效问题1插件配置了但查询结果字段未脱敏排查步骤检查插件是否被正确加载 在DataMaskingInterceptor的afterQuery方法开始处打日志看是否执行。检查注解是否生效 确认实体类字段上的DataMasking注解是Retention(RetentionPolicy.RUNTIME)。检查字段类型 插件中默认只处理String类型的字段。如果敏感字段是其他类型如Long型的手机号需要调整判断逻辑或进行类型转换。检查结果对象类型 插件处理的是MyBatis最终返回的对象。如果你使用了resultMap或者返回的是MapString, Object插件基于Java反射的机制可能会失效。这种情况下需要考虑处理Map类型的结果。问题2分页查询时total总数正确但records列表中的数据被脱敏了而我又需要原始数据用于其他计算原因 分页插件如PaginationInnerInterceptor和我们的脱敏插件都在同一个拦截器链中。脱敏插件处理了最终返回的List数据。解决 这正是需要引入DataMaskingContext忽略机制的场景。在需要原始数据的方法中临时关闭脱敏。public PageUser getPageForExport(PageQuery query) { try { DataMaskingContext.setIgnoreMasking(true); // 执行分页查询此时records里的数据是原始的 PageUser page userMapper.selectPage(new Page(query.getPage(), query.getSize()), query.buildWrapper()); // ... 可以对page.getRecords()进行业务计算 return page; } finally { DataMaskingContext.clear(); } }注意返回给前端时如果仍需脱敏展示可以在Controller层将page对象转换成一个新的DTO页面对象在转换过程中对DTO字段进行脱敏。问题3使用TableField(typeHandler ...)时脱敏失效原因 MyBatis的类型处理器TypeHandler会在结果集映射时将数据库的ResultSet值转换为Java对象。这个转换发生在我们的拦截器handleResultSets方法被调用之前。如果类型处理器已经将值转换并设置到了实体字段中我们的插件再通过反射去获取和设置时操作的就是类型处理器处理后的值。影响 这通常不是问题因为脱敏操作的是Java对象的值。但如果你的类型处理器做了非常特殊的转换可能需要确保脱敏逻辑在类型处理器之后依然适用。大多数情况下两者是兼容的。6.3 生产环境部署要点密钥管理是生命线 Jasypt的加密密钥必须通过安全的方式管理如从云厂商的密钥管理服务获取或由部署脚本从安全存储中读取并设置为环境变量。严禁硬编码在配置文件或代码中。做好监控与审计 对脱敏插件的运行情况添加监控。例如记录脱敏操作的日志在Debug级别并监控其执行耗时确保在大数据量下不会造成性能问题。进行全面的测试单元测试 测试DataMaskingUtil的各种策略是否正确。集成测试 测试插件与MyBatis-Plus的集成包括单表查询、联表查询、分页查询、嵌套结果等场景。忽略功能测试 测试DataMaskingContext的忽略开关是否正常工作。制定数据脱敏规范 在团队内明确哪些字段属于敏感信息必须加DataMasking注解。可以在代码审查Code Review中将其作为一项检查项。回过头看从配置加密到业务数据自动化脱敏这套组合方案确实为我们的系统筑起了一道有效的安全防线。它最大的价值在于将安全能力下沉为一种“默认行为”开发者无需在每一个查询、每一个日志打印点都战战兢兢地思考数据是否该隐藏。只需要在实体类上打一个注解安全就得到了保障。这种“约定大于配置”的方式极大地降低了开发人员的心智负担和出错概率。当然没有银弹它主要解决的是“展示层”的泄露风险。对于数据库存储加密、网络传输加密、前端展示加密如部分数字用*号组件等还需要结合其他方案共同构建纵深防御体系。在数据安全这条路上多一道防线就多一分安心。