当前位置: 首页 > news >正文

SpringBoot+Mybatis数据安全插件:基于拦截器实现字段自动加解密与脱敏

1. 项目概述与核心价值

最近在做一个内部系统,涉及到不少敏感数据的处理,比如用户的手机号、身份证号、邮箱这些。直接明文存库或者在前端展示,心里总是不踏实,万一有个SQL注入或者日志泄露,那就是大问题。市面上当然有成熟的加解密方案和脱敏组件,但要么太重,集成起来麻烦;要么不够灵活,没法针对我们业务里那些特殊的字段处理规则做定制。所以,我就琢磨着,能不能基于我们最熟悉的SpringBoot和Mybatis技术栈,自己动手写一个轻量级的数据安全处理小插件。

这个插件的核心目标很明确:在数据持久化层(DAO层)实现自动化的加解密与脱敏。简单说,就是数据从Java对象进入数据库之前,自动加密;从数据库查询出来映射回Java对象时,自动解密。同时,在数据返回给前端时,能根据配置对特定字段进行脱敏展示(比如手机号显示为138****8888)。这样一来,业务代码几乎不用做任何改动,只需要在实体类的字段上加个注解,安全处理就自动完成了,开发体验非常流畅。

它的价值在于,用最小的侵入性,为SpringBoot + Mybatis项目提供了一层可靠的数据安全兜底。特别适合那些对数据安全有要求,但又不想引入复杂安全中间件的中小型项目。接下来,我就把这个插件的设计思路、实现细节以及我踩过的坑,完整地分享出来。

2. 整体设计与核心思路拆解

2.1 为什么选择Mybatis拦截器作为切入点?

要实现数据在出入库时的自动处理,最关键的是要找到一个合适的“钩子”(Hook)。在Mybatis的体系里,拦截器(Interceptor)正是这样一个完美的切入点。Mybatis的拦截器可以拦截四大核心对象(Executor, StatementHandler, ParameterHandler, ResultSetHandler)的方法执行,允许我们在SQL执行前后插入自定义逻辑。

对于数据安全处理,我们主要关注两个时机:

  1. 参数设置时(入库前):当Java对象作为参数传入,即将被拼接到SQL语句中时,我们需要拦截并对其中的敏感字段进行加密。这对应拦截ParameterHandlersetParameters方法。
  2. 结果集映射时(出库后):当从数据库ResultSet中取出数据,准备设置回Java对象的属性时,我们需要拦截并对敏感字段进行解密。这对应拦截ResultSetHandlerhandleResultSets方法。

选择Mybatis拦截器,而不是在Service层或Controller层做处理,有以下几个好处:

  • 无侵入性:业务代码(Service, Controller)完全感知不到加解密的存在,只需关注业务逻辑。
  • 统一处理:所有通过Mybatis进行的数据库操作(增、删、改、查)都能被覆盖,避免遗漏。
  • 高性能:在ORM框架层面处理,比在业务层循环处理效率更高,尤其是处理批量数据时。

2.2 插件核心架构设计

整个插件围绕“注解驱动”和“拦截器执行”两个核心思想来构建。架构上可以分为三层:

  1. 注解层:定义用于标记字段的注解,这是插件与业务代码交互的契约。
    • @FieldEncrypt:标记需要加解密的字段,可指定加密算法(如AES)、密钥等信息。
    • @FieldSensitive:标记需要脱敏的字段,可指定脱敏策略(如手机号、身份证、邮箱等)。
  2. 核心处理层:包含加解密执行器和脱敏执行器。它们根据注解信息,调用具体的算法完成数据转换。
    • 加解密执行器:持有加密算法实例(如AES实现),提供encryptdecrypt方法。
    • 脱敏执行器:根据@FieldSensitive的策略类型,提供desensitize方法。
  3. 拦截器层:实现Mybatis的Interceptor接口,包含两个核心拦截器。
    • 加密拦截器:绑定到ParameterHandler,在setParameters方法中,遍历参数对象,查找带有@FieldEncrypt注解的字段,调用加密执行器进行加密,然后用加密后的值替换原值。
    • 解密与脱敏拦截器:绑定到ResultSetHandler,在handleResultSets方法中,遍历查询结果对象,先对带有@FieldEncrypt的字段调用解密执行器进行解密,再对带有@FieldSensitive的字段调用脱敏执行器进行脱敏处理。

此外,还需要一个配置类,用于在SpringBoot启动时自动注册这些拦截器到Mybatis的配置中,并初始化加解密所需的密钥等配置。

这种设计确保了插件的高内聚、低耦合,扩展性也很好。如果想增加一种新的加密算法(比如SM4),只需要实现一个新的加解密执行器,并在注解中指定即可,其他部分无需改动。

3. 核心细节解析与实操要点

3.1 自定义注解的定义与解析

注解是插件的“开关”和“配置说明书”。定义时需要考虑可扩展性和实用性。

// 字段加密注解 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface FieldEncrypt { /** * 加密算法类型,默认为AES */ AlgorithmType algorithm() default AlgorithmType.AES; /** * 密钥的配置名称,用于从环境配置中获取,避免硬编码 */ String keyProperty() default "plugin.security.encrypt.key"; } // 字段脱敏注解 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface FieldSensitive { /** * 脱敏策略类型 */ SensitiveType strategy(); } // 算法类型枚举 public enum AlgorithmType { AES, // 未来可扩展 SM4, RSA等 } // 脱敏策略枚举 public enum SensitiveType { /** 手机号:138****8888 */ MOBILE_PHONE, /** 身份证号:110***********1234 */ ID_CARD, /** 邮箱:a***@example.com */ EMAIL, /** 自定义:需配合customPattern使用 */ CUSTOM }

实操要点与避坑

  • 密钥管理:切忌在注解中硬编码密钥字符串!我们通过keyProperty指定一个配置项的名字(如plugin.security.encrypt.key),实际密钥写在应用的application.yml或配置中心里。这样既安全,也便于不同环境(开发、测试、生产)使用不同的密钥。
  • 注解继承:注意,Mybatis在映射结果集时,如果使用了继承,注解是否能被正确识别取决于你的反射工具。通常,我们需要使用ClassUtil.getAllFields(clazz)这样的工具方法来获取类及其所有父类的字段,确保不会遗漏父类中带有注解的字段。
  • 性能考虑:每次操作都通过反射解析注解是比较耗时的。一个常见的优化点是使用缓存。我们可以缓存“类 -> 需要处理的字段列表”的映射关系,避免每次拦截都进行全量反射扫描。

3.2 加解密引擎的实现与选型

加解密是安全的核心,必须保证其可靠性和合规性。

1. 算法选型

  • 对称加密(推荐):如AES。加解密速度快,适合对大量数据进行加密。对于绝大多数内部系统,使用AES-256-GCM模式足以满足安全需求。GCM模式还能同时提供机密性和完整性校验。
  • 非对称加密:如RSA。加解密速度慢,通常用于加密密钥本身(即“信封加密”),而非直接加密数据。在本插件中,如果对安全性要求极高,可以考虑采用“RSA加密AES密钥,AES加密数据”的混合模式,但复杂度会显著增加。

2. AES-GCM实现示例: GCM模式需要Initialization Vector (IV),且IV不需要保密,但绝不能重复使用同一个IV和密钥对。通常将IV和密文一起存储。

@Component public class AesEncryptor implements Encryptor { @Value("${plugin.security.encrypt.key}") private String secretKey; // 从配置读取,Base64编码的密钥 private SecretKeySpec keySpec; private static final String ALGORITHM = "AES/GCM/NoPadding"; private static final int IV_LENGTH = 12; // GCM推荐IV长度为12字节 private static final int TAG_LENGTH = 16; // GCM标签长度128位 @PostConstruct public void init() throws Exception { byte[] decodedKey = Base64.getDecoder().decode(secretKey); this.keySpec = new SecretKeySpec(decodedKey, "AES"); } @Override public String encrypt(String rawData) throws Exception { if (rawData == null) return null; Cipher cipher = Cipher.getInstance(ALGORITHM); byte[] iv = new byte[IV_LENGTH]; SecureRandom random = new SecureRandom(); random.nextBytes(iv); // 每次加密生成随机IV GCMParameterSpec parameterSpec = new GCMParameterSpec(TAG_LENGTH * 8, iv); cipher.init(Cipher.ENCRYPT_MODE, keySpec, parameterSpec); byte[] cipherText = cipher.doFinal(rawData.getBytes(StandardCharsets.UTF_8)); // 组合 IV + 密文 进行存储或传输 byte[] combined = new byte[iv.length + cipherText.length]; System.arraycopy(iv, 0, combined, 0, iv.length); System.arraycopy(cipherText, 0, combined, iv.length, cipherText.length); return Base64.getEncoder().encodeToString(combined); } @Override public String decrypt(String encryptedData) throws Exception { if (encryptedData == null) return null; byte[] combined = Base64.getDecoder().decode(encryptedData); byte[] iv = Arrays.copyOfRange(combined, 0, IV_LENGTH); byte[] cipherText = Arrays.copyOfRange(combined, IV_LENGTH, combined.length); Cipher cipher = Cipher.getInstance(ALGORITHM); GCMParameterSpec parameterSpec = new GCMParameterSpec(TAG_LENGTH * 8, iv); cipher.init(Cipher.DECRYPT_MODE, keySpec, parameterSpec); byte[] plainText = cipher.doFinal(cipherText); return new String(plainText, StandardCharsets.UTF_8); } }

3. 关键注意事项

  • 密钥安全:生产环境的密钥必须妥善保管,绝不能提交到代码仓库。推荐使用配置中心或容器秘钥管理服务。
  • IV管理:如上例,必须使用密码学安全的随机数生成器(SecureRandom)为每次加密生成唯一的IV,并和密文一起存储/传输。
  • 异常处理:加解密过程可能抛出各种异常(如密钥错误、数据被篡改导致认证失败AEADBadTagException)。在拦截器中必须妥善捕获并处理,是记录日志后返回空值,还是直接抛出异常让上层处理,需要根据业务场景决定。我建议在测试环境采用“失败快速”原则直接抛异常,生产环境则可记录详细日志后降级处理(如返回脱敏后的空值)。
  • 字段类型:我们的注解加在String类型字段上最直接。如果字段是其他类型(如Long型的手机号),需要在加解密前后进行类型转换。

3.3 脱敏策略的灵活实现

脱敏的逻辑相对加解密简单,核心是根据策略对字符串进行截取和替换。

@Component public class SensitiveDataProcessor { public String desensitize(String data, SensitiveType type) { if (data == null || data.isEmpty()) { return data; } switch (type) { case MOBILE_PHONE: return data.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); case ID_CARD: if (data.length() == 15) { return data.replaceAll("(\\d{6})\\d{6}(\\d{3})", "$1******$2"); } else if (data.length() == 18) { return data.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2"); } return data; // 格式不符,原样返回或做其他处理 case EMAIL: int atIndex = data.indexOf('@'); if (atIndex > 1) { return data.charAt(0) + "***" + data.substring(atIndex); } return data; case CUSTOM: // 可以结合注解中的自定义正则进行脱敏 // 例如:@FieldSensitive(strategy = SensitiveType.CUSTOM, customPattern = "(?<=\\w{3})\\w(?=\\w{4})") // 处理逻辑... default: return data; } } }

实操心得

  • 保持数据原貌:脱敏仅用于展示。任何需要基于原始数据进行计算、匹配的业务逻辑(如按手机号查询),必须在解密后的原始数据上进行。因此,脱敏操作通常放在返回给前端之前的最后一步,或者在DTO转换时进行。
  • 国际化考虑:不同国家的手机号、身份证号格式不同。如果你的系统面向国际用户,脱敏策略需要可配置化,或者设计得更通用(如保留前N位和后M位)。
  • 性能无虞:脱敏操作是简单的字符串处理,性能开销极小,一般无需特别优化。

4. Mybatis拦截器的实现与集成

这是插件最核心的部分,将注解、加解密、脱敏串联起来。

4.1 加密拦截器实现

加密拦截器负责在SQL参数设置时,对入参对象进行加密。

@Intercepts({ @Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}) }) @Component public class EncryptionInterceptor implements Interceptor { @Autowired private EncryptorManager encryptorManager; // 管理不同算法的加密器 @Autowired private FieldAnnotationCache annotationCache; // 注解缓存 @Override public Object intercept(Invocation invocation) throws Throwable { ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget(); // 通过反射获取ParameterHandler内部的parameterObject(即我们的实体对象) Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject"); parameterField.setAccessible(true); Object parameterObject = parameterField.get(parameterHandler); if (parameterObject != null) { // 处理参数对象(可能是一个对象,也可能是Map、集合等) processParameterObject(parameterObject); } // 继续执行原方法 return invocation.proceed(); } private void processParameterObject(Object parameterObject) throws IllegalAccessException { if (parameterObject instanceof Map) { // 处理Mybatis传递的Map参数,例如@Param注解标注的参数 ((Map<?, ?>) parameterObject).values().forEach(value -> { if (value != null && !isPrimitiveOrWrapper(value.getClass())) { try { encryptFields(value); } catch (Exception e) { throw new RuntimeException("加密字段处理失败", e); } } }); } else if (parameterObject instanceof Collection) { // 处理批量插入/更新 for (Object item : (Collection<?>) parameterObject) { if (item != null && !isPrimitiveOrWrapper(item.getClass())) { encryptFields(item); } } } else if (!isPrimitiveOrWrapper(parameterObject.getClass())) { // 处理单个实体对象 encryptFields(parameterObject); } } private void encryptFields(Object obj) throws IllegalAccessException { List<Field> encryptedFields = annotationCache.getEncryptedFields(obj.getClass()); for (Field field : encryptedFields) { field.setAccessible(true); Object originalValue = field.get(obj); if (originalValue instanceof String) { FieldEncrypt annotation = field.getAnnotation(FieldEncrypt.class); Encryptor encryptor = encryptorManager.getEncryptor(annotation.algorithm()); String encryptedValue = encryptor.encrypt((String) originalValue); field.set(obj, encryptedValue); // 将加密后的值设回字段 } } } // ... 辅助方法 isPrimitiveOrWrapper }

4.2 解密与脱敏拦截器实现

这个拦截器更复杂一些,它需要在Mybatis完成结果集映射后,对对象字段进行解密和脱敏。

@Intercepts({ @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}) }) @Component public class DecryptionAndDesensitizationInterceptor implements Interceptor { @Autowired private EncryptorManager encryptorManager; @Autowired private SensitiveDataProcessor sensitiveDataProcessor; @Autowired private FieldAnnotationCache annotationCache; @Override public Object intercept(Invocation invocation) throws Throwable { // 1. 先执行原方法,获取Mybatis映射后的结果 Object result = invocation.proceed(); // 2. 对结果进行处理 if (result != null) { if (result instanceof ArrayList) { for (Object item : (ArrayList<?>) result) { processObject(item); } } else { // 单个对象结果 processObject(result); } } return result; } private void processObject(Object obj) throws Exception { if (obj == null || isPrimitiveOrWrapper(obj.getClass())) { return; } Class<?> clazz = obj.getClass(); List<Field> encryptedFields = annotationCache.getEncryptedFields(clazz); List<Field> sensitiveFields = annotationCache.getSensitiveFields(clazz); // 先解密 for (Field field : encryptedFields) { field.setAccessible(true); Object currentValue = field.get(obj); if (currentValue instanceof String) { FieldEncrypt annotation = field.getAnnotation(FieldEncrypt.class); Encryptor encryptor = encryptorManager.getEncryptor(annotation.algorithm()); String decryptedValue = encryptor.decrypt((String) currentValue); field.set(obj, decryptedValue); } } // 后脱敏(基于解密后的值) for (Field field : sensitiveFields) { field.setAccessible(true); Object currentValue = field.get(obj); if (currentValue instanceof String) { FieldSensitive annotation = field.getAnnotation(FieldSensitive.class); String desensitizedValue = sensitiveDataProcessor.desensitize((String) currentValue, annotation.strategy()); field.set(obj, desensitizedValue); } } } }

关键点解析

  1. 执行顺序:在handleResultSets方法中,必须先invocation.proceed()让Mybatis完成默认的结果集映射,拿到已经填充了数据库值的对象,然后我们才能对这些对象进行后处理。
  2. 处理多种返回类型:Mybatis查询可能返回单个对象、ListMap,甚至是嵌套结果。上述示例处理了ArrayList(Mybatis默认返回的List实现)和单个对象的情况。对于更复杂的Map或自定义结果处理器,需要额外判断和处理。
  3. 解密与脱敏的顺序必须先解密,再脱敏。因为数据库存储的是加密后的密文,需要先解密得到原始明文,才能对明文进行脱敏操作。这个顺序绝对不能错。

4.3 SpringBoot自动配置与插件注册

为了让插件开箱即用,我们需要提供一个自动配置类。

@Configuration @AutoConfigureAfter(MybatisAutoConfiguration.class) // 在Mybatis自动配置完成后进行 public class DataSecurityPluginAutoConfiguration { @Bean @ConditionalOnMissingBean public EncryptorManager encryptorManager(List<Encryptor> encryptors) { return new EncryptorManager(encryptors); // 收集所有Encryptor实现 } @Bean @ConditionalOnMissingBean public FieldAnnotationCache fieldAnnotationCache() { return new FieldAnnotationCache(); } @Bean public EncryptionInterceptor encryptionInterceptor() { return new EncryptionInterceptor(); } @Bean public DecryptionAndDesensitizationInterceptor decryptionAndDesensitizationInterceptor() { return new DecryptionAndDesensitizationInterceptor(); } /** * 关键:将拦截器注册到Mybatis的SqlSessionFactory中 */ @Bean public ConfigurationCustomizer mybatisConfigurationCustomizer( EncryptionInterceptor encryptionInterceptor, DecryptionAndDesensitizationInterceptor decryptionInterceptor) { return configuration -> { // 添加拦截器到拦截器链 configuration.addInterceptor(encryptionInterceptor); configuration.addInterceptor(decryptionInterceptor); }; } }

META-INF/spring.factories文件中声明这个自动配置类:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.yourcompany.plugin.config.DataSecurityPluginAutoConfiguration

这样,只要项目引入了我们这个插件的JAR包,SpringBoot启动时就会自动注册两个拦截器,完成插件的装载。

5. 使用示例与效果验证

5.1 实体类与Mapper定义

假设我们有一个用户实体User

@Data public class User { private Long id; private String username; @FieldEncrypt(algorithm = AlgorithmType.AES, keyProperty = "plugin.security.encrypt.key") private String mobilePhone; // 手机号,存储时加密 @FieldEncrypt(algorithm = AlgorithmType.AES) @FieldSensitive(strategy = SensitiveType.ID_CARD) private String idCard; // 身份证号,存储加密,查询结果脱敏 private String email; }

Mapper接口和XML文件无需任何特殊改动,照常编写即可。

@Mapper public interface UserMapper { @Insert("INSERT INTO user(username, mobile_phone, id_card, email) VALUES(#{username}, #{mobilePhone}, #{idCard}, #{email})") int insert(User user); @Select("SELECT * FROM user WHERE id = #{id}") User selectById(Long id); }

5.2 应用配置

application.yml中配置加密密钥。

plugin: security: encrypt: key: your-32-byte-base64-encoded-aes-key-here # 示例:使用 `openssl rand -base64 32` 生成

5.3 测试与验证

编写一个简单的Service进行测试。

@Service @Slf4j public class UserService { @Autowired private UserMapper userMapper; public void testPlugin() { User user = new User(); user.setUsername("张三"); user.setMobilePhone("13812348888"); user.setIdCard("110101199001011234"); user.setEmail("zhangsan@example.com"); // 插入数据 userMapper.insert(user); log.info("插入后对象(内存中已加密): {}", user); // 查询数据 User dbUser = userMapper.selectById(user.getId()); log.info("查询结果(已自动解密和脱敏):"); log.info(" - 用户名: {}", dbUser.getUsername()); // 张三 log.info(" - 手机号: {}", dbUser.getMobilePhone()); // 13812348888 (明文,已解密) log.info(" - 身份证: {}", dbUser.getIdCard()); // 110***********1234 (已解密并脱敏) log.info(" - 邮箱: {}", dbUser.getEmail()); // zhangsan@example.com // 可以直接查看数据库,mobile_phone和id_card字段存储的是加密后的密文 } }

控制台输出预期

插入后对象(内存中已加密): User(id=null, username=张三, mobilePhone=EncryptedStringHere..., idCard=EncryptedStringHere..., email=zhangsan@example.com) 查询结果(已自动解密和脱敏): - 用户名: 张三 - 手机号: 13812348888 - 身份证: 110***********1234 - 邮箱: zhangsan@example.com

数据库存储内容mobile_phoneid_card字段的值将是AES加密后的Base64字符串,类似5f4dcc3b5aa765d61d8327deb882cf99...,而非明文。

6. 常见问题、排查技巧与进阶优化

在实际开发和上线过程中,我遇到了不少问题,这里总结一下。

6.1 常见问题排查表

问题现象可能原因排查步骤与解决方案
字段未加密/解密1. 拦截器未生效。
2. 注解未正确扫描。
3. 字段类型非String。
1. 检查@Component扫描包路径是否包含拦截器类。
2. 检查@Intercepts签名是否正确,特别是args参数类型。
3. 在拦截器内打日志,确认是否进入intercept方法及processObject方法。
4. 检查缓存FieldAnnotationCache是否成功加载了目标类的字段信息。
5. 确认加密字段是否为String类型,或其他支持的类型。
加解密时报错(如InvalidKeyException)1. 密钥配置错误或长度不符。
2. 加密数据被篡改或IV损坏。
1. 确认application.yml中的密钥配置项名称与注解中keyProperty一致。
2. 确认密钥是Base64编码的,且长度符合算法要求(AES-256需32字节)。
3. 检查加解密代码中SecretKeySpec的生成是否正确。
4. 对于GCM模式,确认加解密时IV的提取和组合逻辑一致。
脱敏未生效1. 解密未成功,脱敏作用于密文。
2. 脱敏策略不匹配。
3. 结果集类型未覆盖。
1.确保解密拦截器在脱敏之前执行。检查两个拦截器的注册顺序(在代码中addInterceptor的顺序)。
2. 检查@FieldSensitive注解的strategy是否正确。
3. 检查拦截器的processObject方法是否处理了实际返回的结果类型(如Map)。
批量操作性能差每次操作都反射解析类字段。引入FieldAnnotationCache,缓存类与需处理字段的映射关系。使用ConcurrentHashMap存储,键为Class<?>,值为List<Field>
与Mybatis-Plus等插件冲突多个拦截器执行顺序问题。Mybatis拦截器执行顺序与注册顺序相反(后注册先执行)。可以通过实现org.apache.ibatis.plugin.InterceptorgetOrder()方法或实现org.springframework.core.Ordered接口来指定顺序。确保加解密拦截器在关键插件(如分页插件)之后执行。
查询条件加密问题使用加密字段作为WHERE条件时,传入的是明文,但数据库存的是密文,导致查不到。这是本插件的一个局限。解决方案:
1.手动加密:在Service层,对作为查询条件的参数,手动调用加密器加密后再传入Mapper。
2.扩展拦截器:更复杂但优雅的方式是,再实现一个拦截StatementHandler的拦截器,在SQL执行前,解析WHERE条件中的参数,对匹配加密字段的参数进行加密替换。但这涉及SQL解析,复杂度较高。

6.2 进阶优化建议

  1. 支持更多字段类型:目前主要支持String类型。可以扩展支持BigDecimalLocalDate等类型,在加解密时进行String与目标类型的转换。
  2. 多租户密钥隔离:在SaaS系统中,不同租户可能需要不同的加密密钥。可以在注解或拦截器中动态获取当前租户ID,并从密钥管理服务中获取对应的密钥。
  3. 审计日志:在拦截器中可以记录哪些字段在何时被加密或解密,用于安全审计。注意日志本身需要脱敏,避免泄露敏感信息。
  4. 与Spring Cloud Config或Nacos集成:将加密密钥存放在配置中心,实现密钥的动态管理和轮转。
  5. 懒加载(Lazy Loading)处理:如果实体类关联了懒加载的集合属性,在拦截器处理主对象时,这些集合可能未被初始化(是代理对象)。需要小心处理,避免触发不必要的懒加载查询,或者忽略对代理对象的字段处理。
  6. 单元测试:务必为插件编写完善的单元测试和集成测试,覆盖单字段、多字段、嵌套对象、批量操作、各种SQL语句(INSERT, UPDATE, SELECT)等场景,确保插件行为符合预期。

6.3 一个真实的“坑”:Mybatis的TypeHandler冲突

我在一次集成中发现,如果一个字段同时被标注了@FieldEncrypt和Mybatis的@TypeHandler(例如,用于处理枚举类型),可能会出现问题。因为TypeHandler会在拦截器之后进行类型转换。这意味着,拦截器加密后的String值,在存入数据库前,又被TypeHandler当成枚举或其他类型处理了,导致报错或数据错误。

解决方案:调整处理逻辑。对于有自定义TypeHandler的字段,加解密操作应该在TypeHandler内部完成,或者避免同时使用两种机制。一个更通用的思路是,在拦截器中,通过MetaObject获取字段的TypeHandler,如果存在且不是默认的StringTypeHandler,则跳过该字段的自动加解密,交由业务方在TypeHandler中自行处理安全逻辑。这增加了复杂性,但保证了兼容性。

这个小插件从构思到稳定运行,花了差不多两周的业余时间。最大的收获不是代码本身,而是对Mybatis拦截器机制、SpringBoot自动装配以及数据安全实践有了更深的理解。它现在安静地运行在我们好几个内部系统里,开发者几乎无感知,但数据安全多了一份保障。如果你也在用SpringBoot和Mybatis,不妨试试自己实现一个,过程中遇到的每一个问题,都是绝佳的学习机会。

http://www.cnnetsun.cn/news/3180738.html

相关文章:

  • Playwright:现代Web自动化与爬虫的瑞士军刀,从原理到实战
  • 逆向工具性能终极对决:radare2、IDA Pro、Ghidra、Binary Ninja深度横评
  • Selenium Grid与Docker容器化:构建标准化自动化测试环境实战
  • 生成式AI工程化落地:五层技术栈与核心场景实战指南
  • Vue3+TypeScript实现Web端微信扫码登录:纯前端内嵌方案全解析
  • PyCharm+Selenium+Python自动化测试环境搭建与实战指南
  • AI驱动的数据库安全:从行为分析到智能威胁检测实战
  • ACE Data Cloud 的 Sora 2 API,为什么适合拿来做对外营销内容
  • 终极AI角色对话平台:用SillyTavern打造专属虚拟伙伴的完整指南
  • React Three Fiber:React 生态中的 3D 渲染方案
  • 鸿蒙物理 108 篇 第七十三篇 六合空间全域架构
  • 复杂异形檐口铝板幕墙安装技术
  • Struts2漏洞检测工具:原理、实战与安全防御体系构建
  • 【复现】新型电力系统下多分布式电源接入配电网承载力评估方法研究(Matlab代码实现)
  • XUnity.AutoTranslator:让Unity游戏告别语言障碍的智能翻译引擎
  • AI海报生成痛点破解:图层分离技术实现二次编辑
  • 终极英雄联盟辅助神器:Seraphine智能战绩查询与自动化工具完整指南
  • uos-network-exporter故障排查:常见问题解决方案与调试技巧
  • NestOS-Config存储配置详解:Btrfs、NFS与加密存储支持
  • Hey项目完全指南:如何快速上手这个革命性命令行AI工具
  • 如何快速上手MLCacheDirect?从安装到基本使用的完整指南
  • gala离线部署完全手册:没有外网也能轻松搭建智能运维平台 [特殊字符]
  • 深入解析Hey架构:揭秘命令行AI智能体的核心设计原理
  • UTBotJava CLI命令行工具完全指南:从安装到高级用法
  • 如何快速部署xlin-sbom?Docker Compose零依赖安装指南
  • OpenEuler Certificate Center高级功能:自动化证书轮换与多CA管理策略
  • MIL 增量学习实战:基于注意力与原型映射,3数据集F1分数提升2-4%
  • openEuler/QoS-Deployment-Test:网络QoS隔离机制深度解析与实战
  • 5个关键步骤:如何在 openEuler 项目中实施 SBOM 标准
  • 快速入门 openEuler SBOM 标准:10分钟创建你的第一个软件物料清单