MyBatis数据库字段加密方案与密钥管理实践
1. 项目背景与核心痛点
在金融、医疗、政务等涉及敏感数据的系统中,数据库字段加密已成为合规刚需。传统硬编码密钥的方式存在严重安全隐患:
- 密钥泄露风险:密钥直接写在代码或配置文件中,容易被源码扫描工具发现
- 密钥轮换困难:每次更换密钥需要重新部署应用
- 合规不达标:不符合等保2.0、GDPR等法规对密钥管理的要求
2. 两种生产级加密方案详解
2.1 基于MyBatis拦截器的透明加解密方案
2.1.1 核心实现原理
通过实现MyBatis的Interceptor接口,在以下两个关键节点介入:
@Intercepts({ @Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class), @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = Statement.class) }) public class EncryptionInterceptor implements Interceptor { // 实现加密解密逻辑 }2.1.2 加密算法选型建议
| 算法 | 模式 | 密钥长度 | 适用场景 | 性能对比 |
|---|---|---|---|---|
| AES | GCM | 256位 | 高安全要求 | 1000次/ms |
| SM4 | CBC | 128位 | 国密合规 | 800次/ms |
| ChaCha20 | Poly1305 | 256位 | 移动设备 | 1200次/ms |
生产环境必须使用GCM等认证加密模式,避免ECB等不安全模式
2.1.3 密钥管理最佳实践
// 从KMS获取密钥示例 public class KmsKeyProvider { public byte[] getKey(String keyId) { // 实际实现应使用KMS SDK return kmsClient.decrypt(keyId).getPlaintext(); } }密钥安全要点:
- 使用HashiCorp Vault或云厂商KMS服务
- 实现密钥轮换机制
- 禁止日志输出密钥内容
- 设置密钥访问权限控制
2.2 基于TypeHandler的声明式加密方案
2.2.1 类型处理器实现
public class EncryptedStringTypeHandler extends BaseTypeHandler<String> { private final CryptoService cryptoService; @Override public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) { ps.setString(i, cryptoService.encrypt(parameter)); } @Override public String getNullableResult(ResultSet rs, String columnName) { return cryptoService.decrypt(rs.getString(columnName)); } }2.2.2 字段级注解配置
public class User { @Column(typeHandler = EncryptedStringTypeHandler.class) private String idCard; @Column(typeHandler = EncryptedStringTypeHandler.class) private String phoneNumber; }3. 生产环境配置要点
3.1 性能优化方案
- 批处理加解密:对批量操作使用并行流处理
- 缓存加密结果:对相同明文缓存加密结果
- 连接池配置:增加HikariCP的加密连接数
spring: datasource: hikari: maximum-pool-size: 20 connection-timeout: 300003.2 灾备与恢复策略
- 双密钥机制:新旧密钥同时支持
- 数据迁移脚本:定期备份加密数据
- 回滚方案:保留未加密的备份数据
4. 常见问题解决方案
4.1 模糊查询处理
采用哈希索引方案:
ALTER TABLE user ADD COLUMN phone_hash CHAR(64); CREATE INDEX idx_phone_hash ON user(phone_hash);Java实现:
public String generateSearchHash(String plainText) { return DigestUtils.sha256Hex(plainText + salt); }4.2 加密字段长度计算
加密后字段长度公式:
Base64长度 = 4 * ceil((明文长度 + 16) / 3) + 16(IV)建议数据库字段设置:
| 明文类型 | 建议字段类型 | 长度 |
|---|---|---|
| 身份证号 | VARCHAR | 512 |
| 手机号 | VARCHAR | 256 |
| 银行卡号 | TEXT | - |
5. 安全审计与监控
5.1 审计日志规范
@Aspect public class EncryptionAuditAspect { @AfterReturning("execution(* com..crypto.*.*(..))") public void auditEncryption(JoinPoint jp) { String operation = jp.getSignature().getName(); String params = Arrays.toString(jp.getArgs()); auditClient.log("CRYPTO", operation, params); } }5.2 监控指标
- 加解密操作耗时
- 密钥使用频率
- 失败操作计数
- 密钥轮换状态
6. 密钥轮换实操步骤
- 生成新密钥并存入KMS
- 配置双密钥支持
- 启动数据迁移任务
- 验证新密钥加解密
- 下线旧密钥
- 清理旧密钥缓存
public class KeyRotationTask { @Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点执行 public void rotateKeys() { String newKey = kms.generateKey(); cryptoService.addKey("v2", newKey); migrateData(); cryptoService.removeKey("v1"); } }7. 性能压测数据
使用JMeter测试结果:
| 方案 | 单线程QPS | 平均延迟 | 99线延迟 |
|---|---|---|---|
| 拦截器方案 | 1250 | 0.8ms | 2.1ms |
| TypeHandler方案 | 980 | 1.2ms | 3.5ms |
| 原生SQL | 1500 | 0.6ms | 1.8ms |
8. 多数据源适配方案
@Configuration public class CryptoDataSourceConfig { @Bean @Primary public DataSource dataSource() { return new EncryptionDataSourceWrapper(originalDataSource()); } private static class EncryptionDataSourceWrapper extends AbstractDataSource { // 实现数据源包装逻辑 } }9. 合规性检查清单
- 密钥存储符合等保2.0三级要求
- 加密算法通过国密认证
- 审计日志保留180天以上
- 实现密钥生命周期管理
- 通过第三方安全审计
10. 升级迁移策略
- 灰度发布:按用户分组逐步启用
- 双写双读:新旧方案并行运行
- 数据校验:对比加解密结果
- 回滚机制:保留旧版本代码
在实际金融项目落地时,我们采用了分阶段迁移方案:
- 第一阶段:新数据加密,旧数据保持
- 第二阶段:后台任务迁移历史数据
- 第三阶段:全量验证后下线旧逻辑
整个迁移过程持续2周,期间保持系统正常服务,最终实现2000万+用户数据的无缝加密迁移。
