Spring Boot + MyBatis 实现数据库字段级加密:基于TypeHandler的透明加解密方案
2026/6/24 7:15:40 网站建设 项目流程

1. 项目概述:为什么我们需要字段级加密?

在开发一个用户管理系统时,我遇到了一个经典难题:用户的手机号、身份证号这类敏感信息,明文存储在数据库里总让人心里不踏实。虽然数据库本身有访问控制,但万一发生拖库、或者有内鬼直接查看数据库,这些信息就完全暴露了。全库加密太重,会影响所有查询性能;在应用层每个地方手动加解密又容易遗漏,代码侵入性太强。这时候,字段级加密就成了一个非常优雅的折中方案——只对特定的敏感字段进行加密存储,查询时自动解密,对业务代码几乎透明。

Spring Boot 和 MyBatis 是 Java 后端开发中最主流的组合之一。MyBatis 强大的灵活性,让我们有机会在 SQL 执行的生命周期中插入自定义逻辑,这正是实现字段级自动加解密的绝佳切入点。这个方案的核心,就是利用 MyBatis 的TypeHandler(类型处理器)Interceptor(拦截器),在数据进出数据库的瞬间,完成加密和解密操作,让业务层像操作普通字符串一样操作加密字段。

简单来说,我们的目标是:在User实体中,标注@EncryptedFieldphone字段,在通过 MyBatis 存入数据库时自动变成一串密文;从数据库查询出来映射到实体时,又自动变回明文。业务层的ServiceController完全感知不到这个过程。

2. 核心思路与架构设计

实现字段级加密,关键在于选择一个合适的“钩子”来嵌入我们的加解密逻辑。经过评估,主要有两个核心组件可供选择,它们各有优劣,适用于不同场景。

2.1 方案选型:TypeHandler vs Interceptor

MyBatis TypeHandler(类型处理器)它的职责是处理 Java 类型与 JDBC 类型之间的转换。当 MyBatis 为 PreparedStatement 设置参数,或从 ResultSet 中获取结果时,如果字段类型匹配,就会调用对应的 TypeHandler。

  • 优点:实现简单、直接,与单个字段类型强绑定。加密和解密逻辑集中在setParametergetResult两个方法里,非常清晰。
  • 缺点:粒度较粗,通常以 Java 类型(如String)为单位。如果想实现“同一个String类型,有的字段加密有的不加密”,就需要更复杂的判断逻辑(例如结合自定义注解)。它主要作用于参数设置和结果映射阶段。

MyBatis Interceptor(拦截器)它可以拦截 MyBatis 执行过程中的核心方法,例如Executorupdatequery方法,能接触到完整的 SQL 语句和参数对象。

  • 优点:能力强大,粒度可粗可细。我们可以在执行 SQL 前,解析语句,修改其中的参数(加密);也可以在获取结果后,遍历结果对象,对特定字段进行解密。它更灵活,能实现基于注解的、精细化的字段控制。
  • 缺点:实现相对复杂,需要理解 MyBatis 的内部执行流程,并且对 SQL 的解析和处理需要谨慎,避免性能开销或引入错误。

我们的选择与理由对于纯粹的字段级加密,TypeHandler 是更简单、更直接的选择。它的工作模式天然契合“数据类型转换”的场景,即明文(Java String)到密文(数据库 String)的转换。我们只需要为需要加密的字段类型(通常是String)注册一个自定义的EncryptedStringTypeHandler即可。通过配合自定义注解(如@EncryptedField),我们可以在 TypeHandler 内判断当前处理的字段是否被注解标记,从而决定是否进行加解密。这种方案侵入性低,易于理解和维护。

因此,本方案将采用自定义注解 + 增强型 TypeHandler作为核心。同时,我们会设计一个可插拔的加解密服务接口,以便轻松替换不同的加密算法(如 AES、SM4)。

2.2 整体架构与数据流

整个方案的组件交互和数据流向如下:

  1. 实体层 (Entity):使用@EncryptedField注解标记需要加密的字段。

    public class User { private Long id; private String username; @EncryptedField // 关键注解 private String phone; @EncryptedField private String idCard; // getters and setters }
  2. 加解密服务 (CryptoService):一个独立的服务接口,提供encrypt(String plainText)decrypt(String cipherText)方法。默认实现使用 AES 算法。

  3. 类型处理器 (EncryptedStringTypeHandler):继承BaseTypeHandler<String>

    • setNonNullParameter:当 MyBatis 向 PreparedStatement 设置参数时,如果该参数对应的字段被@EncryptedField注解,则调用CryptoService.encrypt()后设置。
    • getNullableResult:当 MyBatis 从 ResultSet、CallableStatement 获取 String 类型结果时,判断该结果列对应的 Java 字段是否被@EncryptedField注解,如果是则调用CryptoService.decrypt()
  4. MyBatis 配置:在mybatis-config.xml或 Spring Boot 配置中,注册这个自定义的TypeHandler。或者,更推荐使用扫描包的方式自动注册。

  5. 业务层:像平常一样使用 MyBatis 的Mapper进行增删改查,无需关心加解密细节。

数据流示例(插入用户)Controller -> Service -> Mapper#insert(User)->MyBatis 调用EncryptedStringTypeHandler.setParameter-> 发现phone字段有@EncryptedField-> 调用CryptoService.encrypt(“13800138000”)-> 将密文设置到 SQL 参数中 -> 执行 INSERT。

注意:一个关键的局限性。由于加密后数据已变形,基于加密字段的模糊查询(LIKE ‘%xxx%’)、范围查询(BETWEEN)和排序(ORDER BY)将完全失效。这是所有应用层字段加密方案的固有缺陷。如果业务需要这些功能,需要考虑其他方案,如数据库透明加密(TDE)或使用保序加密等特殊算法(但性能和安全强度会折中)。

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

3.1 加解密服务(CryptoService)的设计

加解密服务是整个方案的安全基石,必须设计得健壮且可扩展。

1. 密钥管理这是安全的重中之重。绝对禁止将密钥硬编码在代码中。

  • 推荐方案:将密钥存储在环境变量、配置中心(如 Apollo、Nacos)或专用的密钥管理服务(KMS)中。在应用启动时读取。
  • 代码示例(基于环境变量)
    @Component public class AesCryptoService implements CryptoService { private final SecretKeySpec secretKey; private final String transformation = “AES/ECB/PKCS5Padding”; // 示例,ECB模式不建议用于生产 public AesCryptoService(@Value(“${encrypt.aes.key}”) String base64Key) { byte[] key = Base64.getDecoder().decode(base64Key); this.secretKey = new SecretKeySpec(key, “AES”); } // ... 加解密方法实现 }
    • transformation指定了算法、模式和填充方式。AES/ECB/PKCS5Padding是一个简单示例,但ECB 模式不安全,因为它会导致相同的明文块加密成相同的密文块,容易受到模式分析攻击。生产环境应使用CBC 或 GCM模式,并需要妥善管理初始向量(IV)。

2. 算法选择与IV(初始化向量)

  • AES-CBC 模式:需要一个随机的、不可预测的 IV。IV 不需要保密,但必须唯一。通常将 IV 和密文一起存储(例如,将IV + 密文拼接后再 Base64 编码存库)。解密时先分离出 IV。
  • AES-GCM 模式:同时提供加密和认证,更安全。它也会产生一个随机 IV(在 GCM 中常称为 Nonce),同样需要和密文一起存储。
  • 国密算法 SM4:如果需要符合国内安全标准,可以轻松替换为 SM4 算法。只需实现一个Sm4CryptoService,并在配置中替换AesCryptoService的 Bean 即可。

3. 加解密方法实现要点

@Override public String encrypt(String plainText) { try { Cipher cipher = Cipher.getInstance(transformation); // 如果是CBC或GCM模式,需要生成IV byte[] iv = new byte[16]; // AES块大小 SecureRandom random = new SecureRandom(); random.nextBytes(iv); IvParameterSpec ivSpec = new IvParameterSpec(iv); cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); // 将IV和密文拼接后编码 byte[] combined = new byte[iv.length + encrypted.length]; System.arraycopy(iv, 0, combined, 0, iv.length); System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length); return Base64.getEncoder().encodeToString(combined); } catch (Exception e) { throw new RuntimeException(“Encryption failed”, e); } }

解密方法是逆过程,先从 Base64 解码,分离出 IV,然后用 IV 和密钥初始化解密模式的 Cipher。

3.2 自定义注解与字段识别

我们需要一个注解来标记哪些字段需要被加密处理器处理。

@Retention(RetentionPolicy.RUNTIME) // 运行时保留 @Target(ElementType.FIELD) // 只能用于字段 @Documented public @interface EncryptedField { // 可以扩展,例如指定不同的加密算法版本或密钥标识 }

TypeHandler中,我们需要判断当前正在处理的字段是否带有此注解。但TypeHandler的接口方法并不直接提供Field信息。这里需要一个技巧:利用 MyBatis 的MappedStatement和运行时反射,或者更简单地,TypeHandler中不直接判断,而是通过配置来绑定

更实用的做法:我们为加密字段专门定义一个类型,比如EncryptedString。但这会污染实体模型。另一种方法是,在注册TypeHandler时,明确指定它只用于处理String类型,然后TypeHandler内部,我们无法直接知道当前是哪个字段。因此,更常见的实践是:

  1. 创建一个通用的EncryptedTypeHandler
  2. 在 MyBatis 的配置文件中,只为特定的字段单独指定这个 TypeHandler,而不是全局注册给所有String类型。这样,只有被指定的字段才会走加密逻辑。

如何在 Spring Boot 中为特定字段指定 TypeHandler?在 MyBatis 的 Mapper XML 文件中:

<resultMap id=“userResultMap” type=“User”> <id property=“id” column=“id”/> <result property=“username” column=“username”/> <!-- 为 phone 和 idCard 字段指定自定义的 TypeHandler --> <result property=“phone” column=“phone” typeHandler=“com.example.handler.EncryptedStringTypeHandler”/> <result property=“idCard” column=“idCard” typeHandler=“com.example.handler.EncryptedStringTypeHandler”/> </resultMap>

在插入或更新时,也需要在#{}中指定typeHandler

<insert id=“insertUser”> INSERT INTO user(username, phone, id_card) VALUES(#{username}, #{phone, typeHandler=com.example.handler.EncryptedStringTypeHandler}, #{idCard, typeHandler=com.example.handler.EncryptedStringTypeHandler}) </insert>

这种方式虽然配置稍显繁琐,但意图非常清晰,且不会影响其他普通字符串字段。

3.3 增强型TypeHandler的实现

如果我们希望结合注解,实现一定程度的自动化,可以尝试在TypeHandler中通过线程上下文或反射来获取字段信息,但这会变得复杂且可能影响性能。一个折中的增强型TypeHandler实现如下:

它接收一个CryptoService,并在加解密时,尝试判断当前操作的属性是否被@EncryptedField注解。但如前所述,在标准的setParametergetResult中很难直接获取。一个变通方法是:假设所有经过此 Handler 的 String 都需要加解密。那么,我们只需要在实体类中,把需要加密的字段的settergetter类型改为EncryptedString(一个包装类),而TypeHandler就处理这个包装类。这样逻辑就清晰了,但改变了实体字段类型。

考虑到复杂性和清晰度,我强烈推荐使用上述“在 XML 中显式指定typeHandler”的方式。它简单、直观、无魔法,符合 MyBatis 的设计哲学。接下来我们按这种方式实现一个标准的TypeHandler

4. 完整实现步骤与代码

我们按照“显式配置”的方案来实现,分为四个步骤:创建加解密服务、创建 TypeHandler、配置 MyBatis 映射、测试验证。

4.1 第一步:实现加解密服务接口

首先定义接口,然后提供 AES 实现。

// 1. 接口定义 public interface CryptoService { String encrypt(String plainText); String decrypt(String cipherText); } // 2. AES实现 (使用CBC模式,更安全) @Component public class AesCryptoService implements CryptoService { private static final String ALGORITHM = “AES/CBC/PKCS5Padding”; private static final String CHARSET = “UTF-8”; private final SecretKeySpec secretKey; private final IvParameterSpec ivSpec; // 这里示例使用固定IV,生产环境应为每个加密随机生成并存储 public AesCryptoService(@Value(“${encrypt.aes.key}”) String base64Key, @Value(“${encrypt.aes.iv}”) String base64Iv) { byte[] key = Base64.getDecoder().decode(base64Key); this.secretKey = new SecretKeySpec(key, “AES”); byte[] iv = Base64.getDecoder().decode(base64Iv); this.ivSpec = new IvParameterSpec(iv); } @Override public String encrypt(String plainText) { try { Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(CHARSET)); return Base64.getEncoder().encodeToString(encryptedBytes); } catch (Exception e) { throw new RuntimeException(“加密失败”, e); } } @Override public String decrypt(String cipherText) { try { Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec); byte[] decodedBytes = Base64.getDecoder().decode(cipherText); byte[] decryptedBytes = cipher.doFinal(decodedBytes); return new String(decryptedBytes, CHARSET); } catch (Exception e) { throw new RuntimeException(“解密失败”, e); } } }

重要提示:上述示例为了简化,使用了固定的 IV。在生产环境中,CBC 模式要求每次加密使用不同的随机 IV。你需要修改encrypt方法,使其生成随机 IV,并将IV + 密文一起编码后返回。相应地,decrypt方法需要先解码,分离出 IV 部分,再用它来初始化解密器。GCM 模式也是类似的道理。

4.2 第二步:实现自定义TypeHandler

这个TypeHandler不再关心注解,它只负责调用CryptoService进行转换。

@MappedTypes(String.class) // 声明它处理 Java 的 String 类型 @MappedJdbcTypes(JdbcType.VARCHAR) // 声明它对应 JDBC 的 VARCHAR 类型 public class EncryptedStringTypeHandler extends BaseTypeHandler<String> { private final CryptoService cryptoService; // 通过构造器注入 CryptoService public EncryptedStringTypeHandler(CryptoService cryptoService) { this.cryptoService = cryptoService; } @Override public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { // 对传入的 String 参数进行加密后,设置到 PreparedStatement 中 String encrypted = cryptoService.encrypt(parameter); ps.setString(i, encrypted); } @Override public String getNullableResult(ResultSet rs, String columnName) throws SQLException { String columnValue = rs.getString(columnName); return decryptIfNotNull(columnValue); } @Override public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { String columnValue = rs.getString(columnIndex); return decryptIfNotNull(columnValue); } @Override public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { String columnValue = cs.getString(columnIndex); return decryptIfNotNull(columnValue); } private String decryptIfNotNull(String columnValue) { if (columnValue == null) { return null; } // 假设数据库里该列存储的都是加密后的密文,直接解密 // 注意:这里有个潜在问题,如果该列原本存在未加密的历史数据,解密会失败。 // 解决方案:可在密文前增加版本前缀,如 “v1:密文”,TypeHandler根据前缀判断是否需要及如何解密。 return cryptoService.decrypt(columnValue); } }

4.3 第三步:Spring Boot 配置与 MyBatis 映射

1. 配置密钥application.yml中:

encrypt: aes: key: “你的32字节Base64编码的AES密钥” # 例如通过 `openssl rand -base64 32` 生成 iv: “你的16字节Base64编码的初始向量” # 固定IV示例,生产环境慎用

2. 注册 TypeHandler 为 Spring Bean我们需要让 MyBatis 能使用这个注入了CryptoServiceTypeHandler。在 Spring Boot 中,可以定义一个配置类:

@Configuration public class MyBatisConfig { @Bean public EncryptedStringTypeHandler encryptedStringTypeHandler(CryptoService cryptoService) { return new EncryptedStringTypeHandler(cryptoService); } // 注意:仅仅声明为Bean,MyBatis不会自动用它处理所有String。 // 我们需要在Mapper XML中显式引用这个Bean。 }

但是,在 XML 中通过全类名引用typeHandler时,MyBatis 会自己实例化它,而不会使用 Spring 容器中的 Bean,这导致CryptoService无法注入。为了解决这个问题,我们需要使用MyBatis-Spring 的SpringBootVFS并确保TypeHandler本身是一个 Spring 组件,或者采用另一种方式:SqlSessionFactoryBean中全局注册 TypeHandler

更优方案:通过mybatis.type-handlers-package扫描并自动注册EncryptedStringTypeHandler本身成为一个@Component,并确保它有一个默认构造器(Spring会通过构造器注入CryptoService)。然后在application.yml中配置扫描路径。

@Component @MappedTypes(String.class) public class EncryptedStringTypeHandler extends BaseTypeHandler<String> { private static CryptoService cryptoService; // 通过 @Autowired 注入静态变量(不推荐但可行),或使用 ApplicationContextHolder。 // 推荐:使用 setter 注入 @Autowired public void setCryptoService(CryptoService cryptoService) { EncryptedStringTypeHandler.cryptoService = cryptoService; } public EncryptedStringTypeHandler() { // 默认构造器,MyBatis实例化时需要 } // ... 其他方法实现,使用静态的 cryptoService }

然后在application.yml中配置:

mybatis: type-handlers-package: com.example.handler # 你的TypeHandler所在包

这样,MyBatis 会扫描到这个类并注册。但这种方式下,这个TypeHandler会对所有String类型与VARCHAR的映射生效,这显然不是我们想要的。

结论:最可靠、最清晰的方式,仍然是放弃全局注册和注解自动发现,老老实实在 Mapper XML 中需要加密的字段上,显式指定typeHandler。为此,我们需要一个无需 Spring 注入、能自己获取CryptoServiceTypeHandler。我们可以让CryptoService成为一个静态工具类,或者使用单例模式。为了简单演示,我们修改CryptoService为静态工具类风格(生产环境请考虑更优雅的依赖管理)。

重构:简化版的静态 CryptoUtil 和 TypeHandler

// 加密工具类(示例,生产环境需完善) public class CryptoUtil { private static CryptoService cryptoService; // 由Spring在启动时初始化 public static void setCryptoService(CryptoService service) { cryptoService = service; } public static String encrypt(String text) { return cryptoService.encrypt(text); } public static String decrypt(String text) { return cryptoService.decrypt(text); } } // 在某个 @Configuration 或主类中初始化 @PostConstruct public void initCryptoUtil() { CryptoUtil.setCryptoService(aesCryptoService); } // TypeHandler 修改为使用静态工具类 public class EncryptedStringTypeHandler extends BaseTypeHandler<String> { @Override public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) { ps.setString(i, CryptoUtil.encrypt(parameter)); } @Override public String getNullableResult(ResultSet rs, String columnName) { String val = rs.getString(columnName); return val != null ? CryptoUtil.decrypt(val) : null; } // ... 其他 getNullableResult 方法类似 }

这样,TypeHandler就可以被 MyBatis 直接实例化,并在 XML 中引用了。

3. 编写 Mapper XML

<!-- UserMapper.xml --> <mapper namespace=“com.example.mapper.UserMapper”> <resultMap id=“BaseResultMap” type=“com.example.entity.User”> <id column=“id” property=“id”/> <result column=“username” property=“username”/> <result column=“phone” property=“phone” typeHandler=“com.example.handler.EncryptedStringTypeHandler”/> <result column=“id_card” property=“idCard” typeHandler=“com.example.handler.EncryptedStringTypeHandler”/> </resultMap> <insert id=“insert” parameterType=“User” useGeneratedKeys=“true” keyProperty=“id”> INSERT INTO user (username, phone, id_card) VALUES (#{username}, #{phone, typeHandler=com.example.handler.EncryptedStringTypeHandler}, #{idCard, typeHandler=com.example.handler.EncryptedStringTypeHandler}) </insert> <select id=“selectById” resultMap=“BaseResultMap”> SELECT id, username, phone, id_card FROM user WHERE id = #{id} </select> <!-- 注意:如果需要根据加密字段查询,参数也必须经过相同的TypeHandler处理 --> <select id=“selectByPhone” resultMap=“BaseResultMap”> SELECT id, username, phone, id_card FROM user WHERE phone = #{phone, typeHandler=com.example.handler.EncryptedStringTypeHandler} </select> </mapper>

4.4 第四步:测试验证

编写一个简单的单元测试或直接运行应用测试。

@SpringBootTest class UserMapperTest { @Autowired private UserMapper userMapper; @Test void testEncryption() { User user = new User(); user.setUsername(“testUser”); user.setPhone(“13800138000”); user.setIdCard(“110101199001011234”); userMapper.insert(user); User fetchedUser = userMapper.selectById(user.getId()); System.out.println(“插入后查询:”); System.out.println(“手机号: ” + fetchedUser.getPhone()); // 应显示明文 13800138000 System.out.println(“身份证: ” + fetchedUser.getIdCard()); // 应显示明文 110101199001011234 // 直接查数据库,phone和id_card列应该是Base64编码的密文 // 测试等值查询 User userByPhone = userMapper.selectByPhone(“13800138000”); assertThat(userByPhone).isNotNull(); assertThat(userByPhone.getUsername()).isEqualTo(“testUser”); } }

运行测试,观察数据库中的数据是否为密文,而程序读取出来的实体字段是否为明文。如果一切正常,说明字段级加密方案成功运行。

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

在实际使用中,你肯定会遇到一些坑。下面是我在项目中总结的一些经验和解决方案。

5.1 常见问题速查表

问题现象可能原因解决方案
插入数据时报错:InvalidEncryptedTextException或解密失败1. 加密密钥或IV与解密时不一致。
2. 数据库字段长度不足,密文被截断。
3. 加密后包含特殊字符,在传输或存储时被修改。
1. 检查配置,确保加解密服务使用的密钥/IV完全相同。
2. 将数据库字段类型改为VARCHARTEXT并预留足够长度(AES加密后Base64,长度会增加)。
3. 确保连接池、数据库驱动没有对字符串做不必要的转义。使用Base64编码可避免大部分特殊字符问题。
查询时返回明文,但数据库里是明文(未加密)1. Mapper XML 中未在对应字段的#{}<result>上指定typeHandler
2.TypeHandler未正确注册或生效。
1. 仔细检查 Mapper XML,确保插入/更新和结果映射都指定了typeHandler
2. 检查typeHandler类的全限定名是否正确,以及是否在类路径下。
查询时返回null1.TypeHandlergetNullableResult方法中,解密过程抛出异常被吞没。
2. 数据库该字段本身就是NULL
1. 在TypeHandler的解密方法中添加日志或断点,查看解密前的密文值是否正确。
2. 检查 SQL 查询结果。
日志中显示SQL参数已是密文,但执行报错密文可能包含 SQL 保留字符(如单引号),导致 SQL 语句语法错误。MyBatis 的#{}语法使用的是 PreparedStatement,参数会被正确转义,通常不会有此问题。如果使用${}拼接SQL,则绝对禁止,必须改为#{}
新增字段加密正常,但历史明文数据查询失败TypeHandler默认对所有经过它的数据尝试解密,历史明文数据不符合密文格式,导致解密异常。实现版本化或标识化。例如,在密文前加上前缀{AES},在TypeHandler中判断:如果有前缀则解密,否则直接返回原值。这需要数据迁移或双写支持。

5.2 进阶优化方案

1. 平滑处理已存在的历史数据这是上线时最头疼的问题。方案是采用“版本标识”。

  • 修改CryptoUtil.encrypt,在密文前加上一个版本标识,如{v1}
  • 修改TypeHandlerdecryptIfNotNull方法:判断字符串是否以{v1}开头,如果是,则去掉标识后解密;否则,直接返回原字符串(即历史明文)。
  • 对于历史数据,可以分批跑迁移脚本,读取明文,加密后加上标识写回。

2. 支持多种加密算法定义加密算法枚举,在@EncryptedField注解中增加algorithm()属性。

public @interface EncryptedField { Algorithm algorithm() default Algorithm.AES; } enum Algorithm { AES, SM4 }

TypeHandler中,需要通过某种方式(如从线程上下文、或解析密文头)获取当前字段应使用的算法,然后从一个Map<Algorithm, CryptoService>中选择对应的服务进行加解密。这需要更复杂的TypeHandler工厂模式。

3. 与 MyBatis-Plus 集成如果你使用 MyBatis-Plus,过程类似。你可以在MetaObjectHandler(自动填充器)中尝试处理,但更推荐的方式仍然是使用 MyBatis 原生的TypeHandler。MyBatis-Plus 完全兼容 MyBatis 的配置,只需在实体类字段上使用 MP 的@TableField注解指定typeHandler即可:

@TableField(typeHandler = EncryptedStringTypeHandler.class) private String phone;

这样配置更加简洁,无需修改 XML。

4. 性能考量

  • 加解密是 CPU 密集型操作。如果单次操作数据量极大(如导出全表),可能会对服务端造成压力。
  • 建议在数据库连接池配置和 MyBatis 执行器层面,避免超大规模的批量操作一次性解密。对于列表查询,如果列表很长且包含多个加密字段,解密开销需要关注。
  • 可以考虑在TypeHandler中加入简单的缓存(如使用 ThreadLocal 缓存当前结果集的解密结果),但要小心线程安全和内存泄漏。

5.3 排查技巧实录

场景:测试环境加密正常,上线后部分用户数据解密失败。

  • 第一步,检查密钥一致性:立刻确认生产环境配置文件中的密钥是否与测试环境不同。确保构建部署流程没有覆盖或错误替换配置文件。
  • 第二步,检查数据库编码:曾经遇到过一次,因为数据库表字段是latin1编码,存储 Base64 密文中的某些字符(如+)在某种传输环境下被错误转换,导致解密时 Base64 解码失败。将字段编码改为utf8mb4后解决。
  • 第三步,查看完整密文:从数据库直接复制出密文字段的值,在本地写一个简单的解密测试程序,用同样的密钥解密。如果本地成功,说明问题可能出在应用读取数据库的过程(如结果集处理);如果本地也失败,则问题在密文本身或密钥。
  • 第四步,开启详细日志:在TypeHandlersetParametergetResult方法中加入DEBUG级别日志,打印出入参和出参的摘要(注意不要打印完整密文到日志,以防泄露)。这能帮你确定加解密发生在哪个环节。

字段级加密是一个在安全性和便利性之间取得平衡的优秀方案。它不能防御所有类型的攻击(如数据库文件被窃取后的离线破解),但能极大增加拖库后的数据利用难度,符合“纵深防御”的安全原则。实现的关键在于理解 MyBatis 类型处理器的工作机制,并妥善处理好密钥管理和历史数据迁移问题。希望这份详细的指南能帮助你顺利落地这一功能。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询