# 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 密钥管理方案 #### 开发环境 **方案**:环境变量存储密钥 **配置**: ```bash export ENCRYPTION_KEY=your-256-bit-key-here export ENCRYPTION_IV=your-initialization-vector ``` **优势**: - 简单易用 - 不易泄露到代码库 --- #### 测试环境 **方案**:环境变量 + 配置文件加密 **配置**: ```yaml encryption: key: ${ENCRYPTION_KEY} iv: ${ENCRYPTION_IV} enabled: true ``` **优势**: - 配置灵活 - 支持多环境 --- #### 生产环境 **方案**:密钥管理服务(如阿里云KMS) **架构**: ``` 应用启动 → 从KMS获取密钥 → 缓存到内存 → 使用密钥加密/解密 ``` **优势**: - 密钥集中管理 - 密钥自动轮换 - 审计日志完整 --- ### 2.3 数据迁移方案 #### 迁移步骤 **步骤1:创建加密字段** ```sql 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:批量加密现有数据** ```java @Transactional public void migrateData() { List members = memberRepository.findAll(); for (Member member : members) { if (member.getPhone() != null) { String encrypted = encryptionUtil.encrypt(member.getPhone()); member.setPhoneEncrypted(encrypted); } // 其他字段类似处理 } memberRepository.saveAll(members); } ``` **步骤3:验证加密数据正确性** ```java public void verifyEncryption() { List members = memberRepository.findAll(); for (Member member : members) { if (member.getPhoneEncrypted() != null) { String decrypted = encryptionUtil.decrypt(member.getPhoneEncrypted()); // 验证解密后的数据与原数据一致 assert decrypted.equals(member.getPhone()); } } } ``` **步骤4:删除明文字段** ```sql ALTER TABLE member DROP COLUMN phone; ALTER TABLE member DROP COLUMN id_card; ALTER TABLE member DROP COLUMN bank_card; ``` **步骤5:重命名字段** ```sql 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 ```java @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 ```java @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 ```java @Component @Converter(autoApply = true) public class PhoneEncryptConverter implements AttributeConverter { @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 单元测试 #### 加密/解密功能测试 ```java @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"); }); } } ``` #### 密钥管理测试 ```java @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 集成测试 #### 数据库存储加密测试 ```java @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 性能测试 ```java @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:解密失败** - 应对:保存原始密文,提供手动解密工具 --- ## 八、相关文档 - [改进路线图](../05-PLANS/改进路线图.md) - [EVAL-003-安全性与容错能力评估报告](../03-EVALUATION/EVAL-003-安全性与容错能力评估报告.md) - [SEC-安全设计](../02-ARCHITECTURE/技术架构/SEC-安全设计.md)