dec9085205
- IMPL-001: 响应式编程培训方案 - IMPL-002: 敏感数据加密存储方案 - IMPL-003: 预约高峰期性能优化方案 - IMPL-004: 支付接口幂等性校验方案
14 KiB
14 KiB
IMPL-002: 敏感数据加密存储方案
文档编号: GYM-IMPL-002
版本: v1.0
日期: 2026-04-05
作者: 张翔
状态: 正式发布
文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
|---|---|---|---|
| v1.0 | 2026-04-05 | 张翔 | 创建敏感数据加密存储方案 |
一、需求分析
1.1 问题背景
会员隐私数据、支付信息等敏感数据未加密存储,存在数据泄露风险。
1.2 敏感数据识别
| 数据类型 | 字段名 | 敏感级别 | 加密要求 |
|---|---|---|---|
| 会员手机号 | phone | 高 | AES-256-GCM |
| 会员身份证号 | id_card | 高 | AES-256-GCM |
| 会员银行卡号 | bank_card | 高 | AES-256-GCM |
| 支付密码 | payment_password | 高 | BCrypt |
| 会员地址 | address | 中 | AES-256-GCM |
1.3 加密要求
- 加密算法:AES-256-GCM
- 密钥长度:256位
- 加密模式:GCM(提供认证加密)
- 密钥管理:环境变量 + 密钥管理服务
1.4 成功标准
- 数据安全性提升100%
- 合规性提升
- 加密/解密性能≤10ms
- 数据迁移成功率100%
二、技术方案设计
2.1 加密算法选择
AES-256-GCM优势:
- ✅ 安全性高:256位密钥长度
- ✅ 认证加密:GCM模式提供数据完整性验证
- ✅ 性能优秀:硬件加速支持
- ✅ 广泛应用:业界标准算法
加密流程:
明文 → 生成IV → AES-GCM加密 → Base64编码 → 密文
密文 → Base64解码 → AES-GCM解密 → 明文
2.2 密钥管理方案
开发环境
方案:环境变量存储密钥
配置:
export ENCRYPTION_KEY=your-256-bit-key-here
export ENCRYPTION_IV=your-initialization-vector
优势:
- 简单易用
- 不易泄露到代码库
测试环境
方案:环境变量 + 配置文件加密
配置:
encryption:
key: ${ENCRYPTION_KEY}
iv: ${ENCRYPTION_IV}
enabled: true
优势:
- 配置灵活
- 支持多环境
生产环境
方案:密钥管理服务(如阿里云KMS)
架构:
应用启动 → 从KMS获取密钥 → 缓存到内存 → 使用密钥加密/解密
优势:
- 密钥集中管理
- 密钥自动轮换
- 审计日志完整
2.3 数据迁移方案
迁移步骤
步骤1:创建加密字段
ALTER TABLE member ADD COLUMN phone_encrypted VARCHAR(255);
ALTER TABLE member ADD COLUMN id_card_encrypted VARCHAR(255);
ALTER TABLE member ADD COLUMN bank_card_encrypted VARCHAR(255);
步骤2:批量加密现有数据
@Transactional
public void migrateData() {
List<Member> members = memberRepository.findAll();
for (Member member : members) {
if (member.getPhone() != null) {
String encrypted = encryptionUtil.encrypt(member.getPhone());
member.setPhoneEncrypted(encrypted);
}
// 其他字段类似处理
}
memberRepository.saveAll(members);
}
步骤3:验证加密数据正确性
public void verifyEncryption() {
List<Member> members = memberRepository.findAll();
for (Member member : members) {
if (member.getPhoneEncrypted() != null) {
String decrypted = encryptionUtil.decrypt(member.getPhoneEncrypted());
// 验证解密后的数据与原数据一致
assert decrypted.equals(member.getPhone());
}
}
}
步骤4:删除明文字段
ALTER TABLE member DROP COLUMN phone;
ALTER TABLE member DROP COLUMN id_card;
ALTER TABLE member DROP COLUMN bank_card;
步骤5:重命名字段
ALTER TABLE member CHANGE COLUMN phone_encrypted phone VARCHAR(255);
ALTER TABLE member CHANGE COLUMN id_card_encrypted id_card VARCHAR(255);
ALTER TABLE member CHANGE COLUMN bank_card_encrypted bank_card VARCHAR(255);
三、代码结构设计
3.1 包结构
com.gym.manage.security/
├── encryption/
│ ├── EncryptionUtil.java # 加密工具类
│ ├── KeyManager.java # 密钥管理器
│ └── EncryptionConfig.java # 加密配置
├── converter/
│ ├── PhoneEncryptConverter.java # 手机号加密转换器
│ ├── IdCardEncryptConverter.java # 身份证号加密转换器
│ └── BankCardEncryptConverter.java # 银行卡号加密转换器
└── aspect/
└── EncryptionAspect.java # 加密切面
3.2 核心类设计
EncryptionUtil
@Component
public class EncryptionUtil {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 128;
@Autowired
private KeyManager keyManager;
/**
* 加密
* @param plaintext 明文
* @return 密文(Base64编码)
*/
public String encrypt(String plaintext) {
try {
SecretKey key = keyManager.getKey();
byte[] iv = generateIV();
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
// IV + Ciphertext
ByteBuffer buffer = ByteBuffer.allocate(iv.length + ciphertext.length);
buffer.put(iv);
buffer.put(ciphertext);
return Base64.getEncoder().encodeToString(buffer.array());
} catch (Exception e) {
throw new EncryptionException("加密失败", e);
}
}
/**
* 解密
* @param ciphertext 密文(Base64编码)
* @return 明文
*/
public String decrypt(String ciphertext) {
try {
SecretKey key = keyManager.getKey();
byte[] decoded = Base64.getDecoder().decode(ciphertext);
ByteBuffer buffer = ByteBuffer.wrap(decoded);
byte[] iv = new byte[GCM_IV_LENGTH];
buffer.get(iv);
byte[] encrypted = new byte[buffer.remaining()];
buffer.get(encrypted);
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
byte[] plaintext = cipher.doFinal(encrypted);
return new String(plaintext, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new EncryptionException("解密失败", e);
}
}
private byte[] generateIV() {
byte[] iv = new byte[GCM_IV_LENGTH];
new SecureRandom().nextBytes(iv);
return iv;
}
}
KeyManager
@Component
public class KeyManager {
@Value("${encryption.key}")
private String keyString;
private SecretKey cachedKey;
/**
* 获取加密密钥
* @return SecretKey
*/
public SecretKey getKey() {
if (cachedKey == null) {
cachedKey = generateKey(keyString);
}
return cachedKey;
}
private SecretKey generateKey(String keyString) {
byte[] keyBytes = keyString.getBytes(StandardCharsets.UTF_8);
return new SecretKeySpec(keyBytes, "AES");
}
}
PhoneEncryptConverter
@Component
@Converter(autoApply = true)
public class PhoneEncryptConverter implements AttributeConverter<String, String> {
@Autowired
private EncryptionUtil encryptionUtil;
@Override
public String convertToDatabaseColumn(String attribute) {
if (attribute == null) {
return null;
}
return encryptionUtil.encrypt(attribute);
}
@Override
public String convertToEntityAttribute(String dbData) {
if (dbData == null) {
return null;
}
return encryptionUtil.decrypt(dbData);
}
}
四、测试方案
4.1 单元测试
加密/解密功能测试
@SpringBootTest
public class EncryptionUtilTest {
@Autowired
private EncryptionUtil encryptionUtil;
@Test
public void testEncryptAndDecrypt() {
String plaintext = "13800138000";
// 加密
String ciphertext = encryptionUtil.encrypt(plaintext);
assertNotNull(ciphertext);
assertNotEquals(plaintext, ciphertext);
// 解密
String decrypted = encryptionUtil.decrypt(ciphertext);
assertEquals(plaintext, decrypted);
}
@Test
public void testEncryptSamePlaintextDifferentCiphertext() {
String plaintext = "13800138000";
String ciphertext1 = encryptionUtil.encrypt(plaintext);
String ciphertext2 = encryptionUtil.encrypt(plaintext);
// 相同明文,不同密文(因为IV不同)
assertNotEquals(ciphertext1, ciphertext2);
// 但都能正确解密
assertEquals(plaintext, encryptionUtil.decrypt(ciphertext1));
assertEquals(plaintext, encryptionUtil.decrypt(ciphertext2));
}
@Test
public void testEncryptNull() {
String ciphertext = encryptionUtil.encrypt(null);
assertNull(ciphertext);
}
@Test
public void testDecryptInvalidCiphertext() {
assertThrows(EncryptionException.class, () -> {
encryptionUtil.decrypt("invalid-ciphertext");
});
}
}
密钥管理测试
@SpringBootTest
public class KeyManagerTest {
@Autowired
private KeyManager keyManager;
@Test
public void testGetKey() {
SecretKey key = keyManager.getKey();
assertNotNull(key);
assertEquals("AES", key.getAlgorithm());
assertEquals(32, key.getEncoded().length); // 256位 = 32字节
}
@Test
public void testKeyCache() {
SecretKey key1 = keyManager.getKey();
SecretKey key2 = keyManager.getKey();
// 密钥应该被缓存
assertSame(key1, key2);
}
}
4.2 集成测试
数据库存储加密测试
@SpringBootTest
@Transactional
public class MemberEncryptionTest {
@Autowired
private MemberRepository memberRepository;
@Autowired
private EncryptionUtil encryptionUtil;
@Test
public void testSaveEncryptedPhone() {
Member member = new Member();
member.setPhone("13800138000");
Member saved = memberRepository.save(member);
// 从数据库直接查询(绕过JPA转换器)
String encryptedPhone = jdbcTemplate.queryForObject(
"SELECT phone FROM member WHERE id = ?",
String.class,
saved.getId()
);
// 验证数据库中存储的是加密后的数据
assertNotNull(encryptedPhone);
assertNotEquals("13800138000", encryptedPhone);
// 验证可以正确解密
String decrypted = encryptionUtil.decrypt(encryptedPhone);
assertEquals("13800138000", decrypted);
}
@Test
public void testReadDecryptedPhone() {
// 保存会员
Member member = new Member();
member.setPhone("13800138000");
Member saved = memberRepository.save(member);
// 读取会员
Member found = memberRepository.findById(saved.getId()).orElse(null);
// 验证读取的是解密后的数据
assertNotNull(found);
assertEquals("13800138000", found.getPhone());
}
}
4.3 性能测试
@SpringBootTest
public class EncryptionPerformanceTest {
@Autowired
private EncryptionUtil encryptionUtil;
@Test
public void testEncryptPerformance() {
String plaintext = "13800138000";
int iterations = 1000;
long startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
encryptionUtil.encrypt(plaintext);
}
long endTime = System.currentTimeMillis();
long avgTime = (endTime - startTime) / iterations;
System.out.println("平均加密时间: " + avgTime + "ms");
assertTrue(avgTime < 10, "加密时间应小于10ms");
}
@Test
public void testDecryptPerformance() {
String plaintext = "13800138000";
String ciphertext = encryptionUtil.encrypt(plaintext);
int iterations = 1000;
long startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
encryptionUtil.decrypt(ciphertext);
}
long endTime = System.currentTimeMillis();
long avgTime = (endTime - startTime) / iterations;
System.out.println("平均解密时间: " + avgTime + "ms");
assertTrue(avgTime < 10, "解密时间应小于10ms");
}
}
五、实施步骤
| 步骤 | 任务 | 负责人 | 完成时间 | 验收标准 |
|---|---|---|---|---|
| 1 | 识别敏感数据字段 | 后端开发 | 1天 | 敏感数据字段清单 |
| 2 | 设计加密方案 | 架构师 | 1天 | 加密方案文档 |
| 3 | 实现加密工具类 | 后端开发 | 2天 | 单元测试通过 |
| 4 | 实现JPA转换器 | 后端开发 | 1天 | 单元测试通过 |
| 5 | 数据迁移脚本 | 后端开发 | 1天 | 迁移脚本完成 |
| 6 | 测试验证 | 测试工程师 | 1天 | 所有测试通过 |
六、验收标准
6.1 功能验收
- 敏感数据加密存储
- 数据库中无明文敏感数据
- 加密数据可正确解密
- 通过安全审计
6.2 性能验收
- 加密/解密性能≤10ms
- 数据迁移成功率100%
6.3 安全验收
- 密钥管理安全
- 无密钥泄露风险
- 符合数据安全合规要求
七、风险与应对
7.1 风险识别
风险1:密钥泄露
- 应对:使用密钥管理服务,定期轮换密钥
风险2:性能影响
- 应对:使用硬件加速,优化加密算法
风险3:数据迁移失败
- 应对:备份原数据,分批迁移,验证后删除
风险4:解密失败
- 应对:保存原始密文,提供手动解密工具