恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Spring Boot + MyBatis 全链路数据安全实战:从存储加密到API脱敏
首页
资讯中心
/
Spring Boot + MyBatis 全链路数据安全实战:从存储加密到API脱敏
Spring Boot + MyBatis 全链路数据安全实战:从存储加密到API脱敏
发布时间:2026/8/12 9:55:30
1. 项目概述为什么我们需要全链路数据保护在今天的数字化业务里数据就是核心资产尤其是那些涉及用户隐私、商业机密或金融信息的敏感数据。我见过太多项目开发时只关注功能实现数据安全往往被一句“等上线前再加”给打发了。结果呢要么是数据库明文存储密码要么是日志里打印了完整的身份证号甚至API接口返回了不该返回的字段。等到安全审计或数据泄露事件发生再手忙脚乱地打补丁成本高不说架构也被改得面目全非。“从数据库到API”这个标题精准地描绘了一个现代Web应用数据流转的典型路径数据从客户端提交经过后端服务处理最终持久化到数据库反之数据从数据库查询出来经过服务层加工再通过API返回给前端。这个链条上的任何一个环节泄露都可能导致整体安全防线失效。因此全链路保护不是可选项而是必选项。基于Spring Boot和MyBatis这套国内Java后端最主流的“黄金搭档”我们来实战构建一套敏感数据的加密与脱敏体系。这不仅仅是调用几个加密库那么简单它涉及到持久层拦截、业务逻辑无感、数据透明处理以及性能权衡等一系列工程化问题。我会带你从原理到落地把每个环节的“坑”和“最优解”都捋清楚目标是让你看完就能在自己的项目里安全、优雅地实施。2. 核心设计思路透明化与分层处理全链路数据安全最理想的状态是对业务代码“透明”。也就是说业务开发人员在写增删改查时完全不用关心某个字段是加密的还是明文的他就像操作普通字符串一样去操作user.phoneNumber。加解密和脱敏的逻辑应该由底层框架在数据进出数据库、进出服务时自动完成。2.1 分层处理模型为了实现透明化我设计了一个清晰的三层处理模型存储层加密/解密当数据从Java实体Entity准备写入数据库时自动对标记的敏感字段进行加密当数据从数据库查询出来映射到实体时自动解密。这一层是数据安全的基石确保磁盘上的数据是密文。业务层脱敏在数据离开服务层准备通过API返回给前端时根据字段的敏感级别和当前用户的权限对数据进行脱敏处理如手机号显示为138****1234。这一层关注的是数据的使用安全。日志层过滤确保在任何日志如SQL日志、调试日志、异常日志中都不会打印出敏感信息的明文。这一层常常被忽略却是信息泄露的重灾区。这个模型的关键在于存储层加密和业务层脱敏是解耦的。一个字段如身份证号在数据库里是加密存储的在返回给前端时可能被脱敏只显示后四位也可能根据权限返回完整明文如内部审核页面。这给了我们极大的灵活性。2.2 技术选型与考量为什么选择MyBatis而不是JPA或MyBatis-Plus这里面的考量很实际。MyBatis的拦截器机制MyBatis提供了Interceptor接口允许我们在SQL执行的生命周期中插入自定义逻辑。ParameterHandler参数处理和ResultSetHandler结果集处理这两个拦截点正是实现透明加密解密的绝佳位置。这种基于AOP面向切面编程思想的方式侵入性极低。对比JPAJPA如Hibernate的监听器EntityListeners和属性转换器AttributeConverter也能实现类似功能但在处理复杂类型如JSON和动态场景时灵活性稍逊。且国内很多存量项目基于MyBatis迁移成本高。对比MyBatis-PlusMyBatis-Plus的字段加密功能需要依赖其特定的注解和配置算是一个“开箱即用”的选项。但它的封装有时会限制我们对加密算法、密钥管理策略进行深度定制。从理解和掌控全链路的角度出发从原生MyBatis入手更能吃透原理。对于加密算法AES高级加密标准是当下的不二之选。它对称加密加解密速度快安全性经过长期验证。对于需要不可逆加密的场景如密码存储则使用SHA-256或SM3国密算法进行哈希。密钥管理务必使用专业的密钥管理服务KMS或至少是独立的、安全的配置文件绝对禁止硬编码在代码中。3. 实战构建基于MyBatis拦截器的存储层加密这是整个体系中最核心、最复杂的一环。我们的目标是在PreparedStatement设置参数前加密在从ResultSet提取数据后解密且对业务代码零侵入。3.1 定义敏感字段注解首先我们需要一个标记告诉框架哪些字段需要被特殊处理。/** * 敏感信息注解 * 用于标记实体类中的敏感字段并指定其加密算法和脱敏策略 */ Target(ElementType.FIELD) Retention(RetentionPolicy.RUNTIME) public interface SensitiveField { /** * 加密算法类型默认AES */ Algorithm algorithm() default Algorithm.AES; /** * 脱敏策略默认不脱敏由业务层控制 */ MaskType mask() default MaskType.NONE; /** * 密钥标识用于从KMS获取特定密钥 */ String keyId() default ; } public enum Algorithm { AES, // 对称加密用于手机号、身份证等 SHA256, // 哈希算法用于密码 SM3 // 国密哈希算法 } public enum MaskType { NONE, // 不脱敏 MOBILE, // 手机号138****1234 ID_CARD, // 身份证1101**********1234 NAME, // 姓名张*三 EMAIL // 邮箱a***gmail.com }在实体类中使用它public class User { private Long id; private String username; SensitiveField(algorithm Algorithm.SHA256) private String password; // 密码哈希存储无需解密 SensitiveField(algorithm Algorithm.AES, mask MaskType.MOBILE, keyId user_data_key) private String phoneNumber; // 手机号AES加密存储返回时脱敏 SensitiveField(algorithm Algorithm.AES, mask MaskType.ID_CARD, keyId user_data_key) private String idCard; // 身份证号 // ... getters and setters }3.2 实现核心加密拦截器接下来是实现拦截器。我们需要拦截两个地方ParameterHandler.setParameters: 将Java对象参数设置到SQL语句中时对敏感字段加密。ResultSetHandler.handleResultSets: 从数据库结果集映射到Java对象时对敏感字段解密。Intercepts({ Signature(type ParameterHandler.class, method setParameters, args {PreparedStatement.class}), Signature(type ResultSetHandler.class, method handleResultSets, args {Statement.class}) }) Component Slf4j public class DataSecurityInterceptor implements Interceptor { Autowired private EncryptionService encryptionService; // 加解密服务 Autowired private SensitiveFieldCache fieldCache; // 缓存实体类与敏感字段的映射关系提升性能 private final ObjectFactoryObject objectFactory new DefaultObjectFactory(); Override public Object intercept(Invocation invocation) throws Throwable { // 判断当前拦截的是参数设置还是结果集处理 if (invocation.getTarget() instanceof ParameterHandler) { return processParameter(invocation); } else if (invocation.getTarget() instanceof ResultSetHandler) { return processResult(invocation); } return invocation.proceed(); } private Object processParameter(Invocation invocation) throws Throwable { ParameterHandler parameterHandler (ParameterHandler) invocation.getTarget(); // 通过反射获取当前Mapper方法要操作的参数对象 Field parameterField parameterHandler.getClass().getDeclaredField(parameterObject); parameterField.setAccessible(true); Object parameterObject parameterField.get(parameterHandler); if (parameterObject ! null) { // 加密处理遍历实体对象找到带有SensitiveField注解的字段并进行加密 encryptSensitiveFields(parameterObject); } // 继续执行原方法设置SQL参数 return invocation.proceed(); } private Object processResult(Invocation invocation) throws Throwable { // 先执行原方法获取查询结果 ListObject resultList (ListObject) invocation.proceed(); if (CollectionUtils.isEmpty(resultList)) { return resultList; } // 对结果集中的每个对象的敏感字段进行解密 for (Object resultObject : resultList) { decryptSensitiveFields(resultObject); } return resultList; } private void encryptSensitiveFields(Object obj) throws IllegalAccessException { // 获取该对象类型的所有敏感字段信息使用缓存 ListField sensitiveFields fieldCache.getSensitiveFields(obj.getClass()); for (Field field : sensitiveFields) { field.setAccessible(true); Object value field.get(obj); if (value instanceof String) { SensitiveField annotation field.getAnnotation(SensitiveField.class); String encryptedValue encryptionService.encrypt((String) value, annotation.algorithm(), annotation.keyId()); field.set(obj, encryptedValue); // 将加密后的值设置回字段 } } } private void decryptSensitiveFields(Object obj) throws IllegalAccessException { ListField sensitiveFields fieldCache.getSensitiveFields(obj.getClass()); for (Field field : sensitiveFields) { field.setAccessible(true); Object value field.get(obj); // 注意哈希算法如密码字段不解密 if (value instanceof String) { SensitiveField annotation field.getAnnotation(SensitiveField.class); if (annotation.algorithm() ! Algorithm.SHA256 annotation.algorithm() ! Algorithm.SM3) { String decryptedValue encryptionService.decrypt((String) value, annotation.algorithm(), annotation.keyId()); field.set(obj, decryptedValue); } } } } Override public Object plugin(Object target) { return Plugin.wrap(target, this); } Override public void setProperties(Properties properties) { // 可以读取MyBatis配置中的属性 } }关键点与避坑指南性能缓存SensitiveFieldCache是一个简单的本地缓存可以用Guava Cache实现用于存储Class与ListField的映射关系。通过反射获取注解信息是耗时的操作必须在应用启动时或首次访问时扫描并缓存绝不能每次数据库操作都进行全类扫描。处理嵌套对象上面的示例只处理了实体对象本身的字段。在实际项目中参数或结果可能是Map、集合或者实体内部包含其他对象如User里有一个ListBankCard。拦截器需要递归地处理这些复杂结构这是实现中的一个难点需要仔细设计递归逻辑和边界判断。类型判断只对String类型的字段进行加解密处理是常见做法。如果字段是BigDecimal金额或其他类型需要根据业务需求特殊处理或者在设计时就约定敏感信息用String存储。哈希字段处理对于密码等使用哈希算法的字段在decryptSensitiveFields方法中要跳过。因为哈希是不可逆的我们存储和比较的都是哈希值。3.3 加解密服务实现EncryptionService是加解密的核心它封装了具体的算法实现和密钥管理。Service public class EncryptionServiceImpl implements EncryptionService { Value(${encryption.aes.secret-key}) // 示例实际应从KMS获取 private String aesSecretKey; private SecretKeySpec aesKeySpec; PostConstruct public void init() { // 初始化AES密钥密钥长度必须是16/24/32字节对应AES-128/192/256 byte[] keyBytes Arrays.copyOf(aesSecretKey.getBytes(StandardCharsets.UTF_8), 32); // 补齐或截取至32字节 aesKeySpec new SecretKeySpec(keyBytes, AES); } Override public String encrypt(String plainText, Algorithm algorithm, String keyId) { if (StringUtils.isBlank(plainText)) { return plainText; } try { switch (algorithm) { case AES: return aesEncrypt(plainText, keyId); case SHA256: return sha256Hash(plainText); case SM3: return sm3Hash(plainText); default: throw new IllegalArgumentException(Unsupported algorithm: algorithm); } } catch (Exception e) { log.error(加密失败算法{} keyId: {}, algorithm, keyId, e); // 强烈建议此处抛出自定义异常而不是返回null让上层业务感知到加密失败。 throw new DataEncryptionException(数据加密失败, e); } } private String aesEncrypt(String plainText, String keyId) throws Exception { // 实际项目中应根据keyId从KMS获取对应的密钥 Cipher cipher Cipher.getInstance(AES/ECB/PKCS5Padding); // 示例用ECB模式生产环境建议用CBC或GCM模式 cipher.init(Cipher.ENCRYPT_MODE, aesKeySpec); byte[] encryptedBytes cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(encryptedBytes); // 二进制转Base64字符串便于存储 } private String sha256Hash(String plainText) throws NoSuchAlgorithmException { MessageDigest digest MessageDigest.getInstance(SHA-256); byte[] hashBytes digest.digest(plainText.getBytes(StandardCharsets.UTF_8)); return bytesToHex(hashBytes); // 转换为十六进制字符串 } // ... decrypt方法实现类似略 }重要提示密钥管理是生命线严禁硬编码示例中的Value仅用于演示。生产环境的密钥必须从硬件安全模块HSM、云厂商KMS如阿里云KMS、AWS KMS或至少是独立的、有严格权限控制的配置中心获取。密钥轮转必须制定密钥轮转策略。为不同时期的数据使用不同的密钥标识keyId在解密时根据数据存储的元信息或从密文中解析出的头信息选择正确的密钥。旧密钥在废弃后仍需安全保留一段时间用于解密历史数据。算法与模式AES的ECB模式不安全因为它会导致相同的明文块加密成相同的密文块。生产环境务必使用CBC模式需要IV或更推荐的GCM模式提供认证。示例为了简洁使用了ECB这是需要你修改的关键点。4. 业务层脱敏在API返回前“戴上面具”存储层加密保证了数据“躺”在数据库里是安全的。但当数据需要展示给用户或外部系统时我们常常需要脱敏。脱敏的逻辑应该在业务层或者在序列化JSON转换时完成确保返回给前端的数据是安全的。4.1 基于Jackson的自定义序列化器Spring Boot默认使用Jackson进行JSON序列化。我们可以为标记了SensitiveField的字段注册一个自定义的序列化器。public class SensitiveFieldSerializer extends JsonSerializerString { Override public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException { // 关键如何获取字段上的注解信息 // Jackson提供了ContextualSerializer接口允许我们在创建序列化器时获取字段上下文。 // 这里为了简化假设我们通过一个ThreadLocal或序列化器内部状态来传递脱敏策略。 // 更优雅的做法是使用JsonSerialize(using ...)注解直接指定但这样不够动态。 // 另一种推荐方案使用独立的脱敏工具类在Service层或DTO转换时手动调用。 } }4.2 更实用的方案在Service层或DTO转换时脱敏在实践中我更喜欢将脱敏逻辑放在Service层或专用的工具类中因为它更直观、可控并且可以方便地结合用户权限进行动态脱敏例如管理员看全量普通用户看脱敏。Service public class UserService { Autowired private UserMapper userMapper; Autowired private DataMaskUtil dataMaskUtil; public UserDTO getUserById(Long id, UserRole currentUserRole) { User user userMapper.selectById(id); // 此时user对象中的敏感字段已被拦截器解密 UserDTO dto convertToDTO(user); // 根据当前用户角色决定脱敏程度 if (currentUserRole ! UserRole.ADMIN) { dto.setPhoneNumber(dataMaskUtil.maskMobile(dto.getPhoneNumber())); dto.setIdCard(dataMaskUtil.maskIdCard(dto.getIdCard())); } // 对于非管理员其他字段如邮箱、姓名也进行相应脱敏 return dto; } private UserDTO convertToDTO(User user) { // 使用MapStruct或手动进行属性拷贝 UserDTO dto new UserDTO(); dto.setId(user.getId()); dto.setUsername(user.getUsername()); dto.setPhoneNumber(user.getPhoneNumber()); // 此时是解密后的明文 dto.setIdCard(user.getIdCard()); return dto; } } Component public class DataMaskUtil { private static final Pattern MOBILE_PATTERN Pattern.compile(^(\\d{3})\\d{4}(\\d{4})$); private static final Pattern ID_CARD_PATTERN Pattern.compile(^(\\d{6})\\d{8}(\\w{4})$); public String maskMobile(String mobile) { if (StringUtils.isBlank(mobile) || mobile.length() ! 11) { return mobile; } return mobile.replaceAll((\\d{3})\\d{4}(\\d{4}), $1****$2); } public String maskIdCard(String idCard) { if (StringUtils.isBlank(idCard) || idCard.length() 8) { return idCard; } // 保留前6位地区码和后4位 int length idCard.length(); String prefix idCard.substring(0, 6); String suffix idCard.substring(length - 4); String middle StringUtils.repeat(*, length - 10); return prefix middle suffix; } // ... 其他脱敏方法 }脱敏策略的思考脱敏规则不是一成不变的。对于手机号你可能想隐藏中间4位对于身份证可能想隐藏生日部分对于地址可能只显示到区县。这些规则应该被抽象成可配置的策略模式方便管理和修改。可以将MaskType枚举与具体的MaskStrategy实现类关联起来。5. 日志与调试信息过滤堵住最后一个漏洞即使数据库加密了、API脱敏了如果日志里把SQL参数或完整的异常堆栈信息打印出来一切努力都可能白费。MyBatis的SQL日志、Spring的请求/响应日志、业务代码中的log.debug都是风险点。5.1 定制MyBatis的日志实现MyBatis通过Log接口适配各种日志框架SLF4J, Log4j2等。我们可以包装原有的日志实现在打印前对参数进行脱敏。public class SensitiveDataLog implements org.apache.ibatis.logging.Log { private final Log delegate; // 原始的日志实例 private final SensitiveParamFilter filter; public SensitiveDataLog(String clazz) { this.delegate LogFactory.getLog(clazz); this.filter new SensitiveParamFilter(); } Override public void debug(String s) { delegate.debug(filter.filter(s)); } Override public void debug(String s, Throwable throwable) { delegate.debug(filter.filter(s), throwable); } // ... 实现其他方法info, error, warn等对字符串s进行过滤 }然后在mybatis-config.xml中配置使用我们自定义的日志实现需要实现LogFactory接口。这是一个相对底层的方案需要对MyBatis日志系统有较深理解。5.2 更通用的方案基于日志框架的PatternLayout或Filter以Logback为例我们可以编写一个自定义的Converter在日志事件格式化的过程中替换敏感信息。configuration conversionRule conversionWordmsg converterClasscom.yourcompany.logging.SensitiveMessageConverter / appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender ... /configurationpublic class SensitiveMessageConverter extends MessageConverter { private static final ListPattern SENSITIVE_PATTERNS Arrays.asList( Pattern.compile((phone|mobile|tel)[\]?\\s*[:]\\s*[\]?(1[3-9]\\d{9})[\]?), Pattern.compile((idCard|identity|身份证)[\]?\\s*[:]\\s*[\]?(\\d{17}[\\dxX])[\]?), Pattern.compile((password|pwd|密码)[\]?\\s*[:]\\s*[\]?([^\\\s])[\]?) ); Override public String convert(ILoggingEvent event) { String message event.getFormattedMessage(); for (Pattern pattern : SENSITIVE_PATTERNS) { Matcher matcher pattern.matcher(message); if (matcher.find()) { // 对匹配到的敏感信息进行脱敏替换 message matcher.replaceAll($1\***\); } } return message; } }日志过滤的难点正则表达式无法覆盖所有情况尤其是当敏感信息以复杂对象如JSON形式出现在日志中时。更可靠的方法是在数据源头控制即在打印日志的代码处就使用脱敏后的数据。这要求团队有良好的编码规范对所有可能记录用户输入或数据库查询结果的日志语句进行审查。6. 常见问题、性能考量与进阶优化一套完善的数据安全方案必然会引入额外的复杂性和性能开销。下面是我在多个项目中实践后总结的关键问题和优化点。6.1 常见问题排查表问题现象可能原因排查步骤与解决方案插入数据成功但数据库里是明文1. 拦截器未生效。2. 字段未加SensitiveField注解。3. 加密方法异常被吞没。1. 检查拦截器是否被正确注册到SqlSessionFactory。2. 确认实体类字段注解无误且包路径被扫描到。3. 在encrypt方法内打日志或断点确认是否执行。检查密钥是否正确加载。查询数据时字段值为null或乱码1. 解密失败如密钥错误、算法模式不匹配。2. 字段类型不匹配如用String接BLOB。3. 拦截器未处理到该字段如嵌套对象。1. 检查数据库存储的密文是否完整Base64编码。对比加密和解密使用的密钥、算法、IV是否完全一致。2. 确认数据库字段类型是VARCHAR或TEXT能存下加密后的Base64字符串长度会变长。3. 调试decryptSensitiveFields方法看目标字段是否在sensitiveFields列表中。模糊查询LIKE失效数据库存储的是密文对密文进行LIKE %明文字段%查询无效。这是加密方案的最大挑战。方案一放弃模糊查询改用精确匹配或搜索引擎如ES。方案二使用数据库支持的加密函数如MySQL的AES_ENCRYPT在SQL层进行加密后比较但这会暴露密钥给数据库且失去MyBatis拦截器的透明性。方案三特定场景对需要模糊查询的字段额外存储一个“可检索的密文”或哈希值如拼音首字母哈希但安全性降低。性能明显下降1. 加解密是CPU密集型操作。2. 反射调用频繁。3. 每次数据库操作都扫描类注解。1. 使用性能更好的算法如AES-NI硬件加速。对非核心或历史数据考虑降低加密强度。2. 使用缓存如SensitiveFieldCache避免重复反射。3. 考虑异步加密或对批量操作进行优化。分页查询总数count错误拦截器修改了parameterObject可能影响某些MyBatis插件或分页插件对参数的处理逻辑。检查分页插件如PageHelper与加密拦截器的执行顺序。确保分页查询的count语句使用的参数是未加密的原始参数通常分页插件会复制参数对象。可能需要调整拦截器的Order或修改插件注册顺序。6.2 性能优化实践算法选型AES-GCM模式虽然更安全但比AES-CBC略慢。如果性能压力极大且安全性要求允许可以在评估后使用AES-CBC。对于大量数据的哈希计算如批量密码加密考虑使用更快的哈希算法如SHA-256相对于bcrypt、PBKDF2。缓存一切可缓存的字段注解缓存如前所述必须缓存。密钥缓存从KMS获取密钥有一定网络开销。可以在内存中缓存密钥并设置合理的TTL和刷新机制。Cipher实例创建Cipher实例Cipher.getInstance()也有开销。可以考虑使用ThreadLocal为每个线程缓存一个Cipher实例但要注意线程安全和使用后的正确复位cipher.init()。异步与批处理对于后台任务、数据导入导出等场景如果实时性要求不高可以将加密解密操作放入队列异步处理。对于批量插入/更新可以考虑在拦截器中识别批量操作使用更高效的方式进行。选择性加密不是所有数据都需要加密。明确敏感数据范围只为必要的字段添加注解。可以对数据进行分类核心身份信息身份证、手机强加密一般信息如昵称、地址不加密或弱加密。6.3 进阶动态密钥与多租户支持在SaaS或多租户系统中不同租户客户的数据需要隔离使用相同的加密密钥存在风险。理想方案是为每个租户使用独立的密钥。在注解中指定keyId如上文示例SensitiveField(keyId “tenant_${tenantId}”)。但注解值必须是常量无法在运行时动态注入。使用ThreadLocal或上下文传递在请求入口处如拦截器、过滤器将当前租户ID存入ThreadLocal。在加密服务EncryptionService中根据ThreadLocal中的租户ID去获取对应的密钥。改造实体模型在实体基类中添加tenantId字段。在拦截器中通过实体对象获取其tenantId进而选择密钥。这种方式更干净但需要所有实体都继承基类。public class EncryptionServiceImpl implements EncryptionService { private final ThreadLocalString currentTenantId new ThreadLocal(); public void setCurrentTenantId(String tenantId) { currentTenantId.set(tenantId); } private String getKey(String keyIdFromAnnotation) { String tenantId currentTenantId.get(); // 如果注解中的keyId包含占位符则替换 String resolvedKeyId keyIdFromAnnotation.replace(${tenantId}, tenantId); // 根据resolvedKeyId从KMS获取密钥 return kmsClient.getKey(resolvedKeyId); } }清理上下文务必在请求处理完成后如使用Spring的RequestScope或过滤器finally块清除ThreadLocal中的值防止内存泄漏和租户信息串用。7. 测试策略如何验证你的加密脱敏体系是可靠的安全功能必须经过严格测试不能只靠“看起来没问题”。单元测试加密解密服务SpringBootTest class EncryptionServiceTest { Autowired private EncryptionService encryptionService; Test void testAesEncryptAndDecrypt() { String plainText 13800138000; String encrypted encryptionService.encrypt(plainText, Algorithm.AES, test_key); assertNotNull(encrypted); assertNotEquals(plainText, encrypted); String decrypted encryptionService.decrypt(encrypted, Algorithm.AES, test_key); assertEquals(plainText, decrypted); } Test void testSha256Hash() { String plainText myPassword123; String hash1 encryptionService.encrypt(plainText, Algorithm.SHA256, ); String hash2 encryptionService.encrypt(plainText, Algorithm.SHA256, ); assertEquals(hash1, hash2); // 相同输入哈希值必须相同 // 验证无法解密应抛异常或返回原值取决于设计 assertThrows(UnsupportedOperationException.class, () - { encryptionService.decrypt(hash1, Algorithm.SHA256, ); }); } }集成测试MyBatis拦截器写入测试插入一个带SensitiveField注解的实体然后直接查询数据库验证对应字段是否为密文Base64格式。读取测试在数据库中手动插入一条密文记录然后通过MyBatis查询该记录验证返回的实体对象中该字段是否为解密后的明文。更新测试更新一个已加密的字段验证更新后数据库中的密文是否改变。API层测试脱敏模拟不同角色管理员、普通用户调用查询API验证返回的DTO中敏感字段是否按预期脱敏。使用MockMvc或TestRestTemplate发起HTTP请求断言响应JSON中特定字段的值。日志测试在测试中执行一个会打印敏感信息的操作如记录包含手机号的SQL然后捕获日志输出使用断言验证日志中是否不包含明文敏感信息。性能压测使用JMeter或编写简单的多线程循环测试对比开启和关闭加密拦截器后核心接口的QPS和平均响应时间。评估性能损耗是否在可接受范围内。这套从数据库到API的全链路数据安全方案初看有些复杂但一旦搭建起来就会成为项目基础设施中坚实的一部分。它要求我们在设计之初就将安全纳入考量而不是事后补救。记住没有绝对的安全但通过分层、纵深防御我们可以将风险降到最低。在实际落地时一定要结合具体的业务场景、性能要求和合规标准进行调整并辅以充分的测试和代码审查。