Java实现数据库加密全攻略:从字段级加密到防脱库实战案例
📚 目录导读
- 为什么数据库需要加密?—— 安全威胁与合规需求
- Java加密核心技术选型:JCE、AES、国密SM4对比
- 实战案例一:基于MyBatis-Plus的字段级透明加密(AES/GCM模式)
- 实战案例二:数据库文件(TDE)加密与连接串安全
- 密钥管理的进阶:混淆存储与KMS集成
- 性能与查询的权衡:如何加密后还能模糊搜索?
- 常见问题QA:解密失败、乱码、排序异常怎么办?
- 总结与最佳实践清单
为什么数据库需要加密?—— 安全威胁与合规需求
在当今数据泄露事件频发的背景下,数据库早已不是内网的安全孤岛,无论是《数据安全法》还是GDPR,都对敏感数据(如手机号、身份证、银行卡)的存储提出了强制加密要求。脱库攻击、SQL注入和内部人员泄露是三大主因,单纯依赖数据库访问控制(如权限、防火墙)已不够,需要从“数据存储层”进行加密,即使文件被拷走,也无法直接读取明文。

Java加密核心技术选型:JCE、AES、国密SM4对比
- JCE(Java Cryptography Extension):JDK自带,提供
Cipher类,支持AES、DES等,注意JDK8默认不支持AES-256,需安装无限制权限策略文件,或使用AES-128。 - AES/GCM/NoPadding:推荐使用 GCM模式(Galois/Counter Mode),它同时提供加密和完整性校验(防篡改),比ECB/CBC更安全,但密文会多出16字节的校验标签。
- 国密SM4:国内合规要求(如等保2.0),可通过BouncyCastle库支持,算法与AES类似,密钥长度128位。
选型建议:国际业务用AES-GCM,国内金融/政企用SM4-GCM。
实战案例一:基于MyBatis-Plus的字段级透明加密(AES/GCM模式)
核心思路:在应用层拦截SQL,对敏感字段进行encrypt()和decrypt(),最优雅的方式是使用MyBatis-Plus的TypeHandler。
// 1. 自定义TypeHandler
@MappedTypes(String.class)
public class AesEncryptHandler extends BaseTypeHandler<String> {
private static final String AES_KEY = "your-16-byte-key!"; // 实际需从配置中心读取
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, AESUtil.encrypt(parameter));
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
return AESUtil.decrypt(rs.getString(columnName));
}
// 省略其他两个get方法
}
// 2. 实体类字段标注
@TableName("user")
public class User {
@TableId
private Long id;
@TableField(typeHandler = AesEncryptHandler.class)
private String phone; // 加密存储
private String name; // 明文存储
}
// 3. AES工具类(使用AES/GCM)
public static String encrypt(String data) throws Exception {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(AES_KEY.getBytes(), "AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] cipherText = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
// 返回 Base64编码,注意包含IV
return Base64.getEncoder().encodeToString(cipherText);
}
痛点:这种方式无法在数据库层做排序、分组查询,因为存储的是密文。
实战案例二:数据库文件(TDE)加密与连接串安全
如果不想改代码,可使用数据库原生透明数据加密(TDE),以MySQL为例:
- 表空间加密(MySQL 8.0+):
CREATE TABLE t (col VARCHAR(10)) ENCRYPTION='Y',使用keyring_file插件管理密钥。 - Java连接串加固:使用
SSL连接(useSSL=true&requireSSL=true),并配置serverCertificateKeyStoreUrl,防止中间人攻击。
注意:TDE只保护磁盘文件,应用层拿到数据后仍是明文,适合“防脱库”但不防“内鬼”。
密钥管理的进阶:混淆存储与KMS集成
最落地的方案:密钥不要写死在代码里! 建议如下:
- 混淆存储:将密钥拆分成三段,分别存放在环境变量、配置文件(非Git仓库)、启动参数中,运行时拼接。
- 对接KMS(如阿里云KMS、AWS KMS):每次启动应用时通过SDK获取明文密钥,内存中缓存,流程如下:
// 启动时拉取 String key = kmsClient.getSecretValue("my-db-key").getSecretString(); System.setProperty("db.key", key); - 轮转机制:定时更换密钥,并用旧密钥解密历史数据后重新加密。
性能与查询的权衡:如何加密后还能模糊搜索?
这是业界难题,常用方案:
- 索引脱敏:对手机号中间4位做脱敏后建立索引(如
138****1234),查询时先通过脱敏值定位,再精确解密。 - 盲索引(Blind Index):使用HMAC-SHA256对明文生成固定长度的哈希值,存为
phone_hash列,查询时对输入值做同样哈希,先匹配哈希,再解密。 - 分段加密:将身份证号按规则拆成3段,分别加密并存储为3列,支持前缀或后缀模糊查询。
常见问题QA:解密失败、乱码、排序异常怎么办?
Q1:解密后中文乱码?
A:加密前必须明确UTF-8编码,解密后必须用UTF-8解码,如果用了AES/GCM,注意Base64编码时不要丢填充符。
Q2:加密字段无法排序?
A:只对敏感字段加密,非敏感字段保持明文,如果必须排序,先单独查出所有数据,在内存中用解密后的值排序(数据量大时不建议)。
Q3:数据库迁移或备份恢复后,解密失败?
A:确认密钥文件是否已备份,如果是TDE,需同时备份keyring文件。
总结与最佳实践清单
- 明确加密范围:仅加密真正敏感的字段,如手机号、邮箱、地址。
- 安全高于性能:优先使用
AES-GCM,避免使用不安全的ECB模式。 - 密钥定期轮换:建立密钥版本机制。
- 日志脱敏:打印SQL日志时,需将参数中的密文替换为(防止明文泄漏)。
- 测试完备:加密后的字段长度变长,需调整数据库字段
VARCHAR长度(密文Base64后为原长度1.33倍+16)。
最后建议:加密是最后一道防线,应结合防火墙、WAF、数据库审计、最小权限原则共同构建立体防护,案例代码已考虑GCM模式的IV随机性,避免重复使用同一密钥加密相同明文导致密文相同的问题。