Files
gym-manage/docs/06-IMPLEMENTATION/IMPL-002-敏感数据加密存储方案.md
T
张翔 dec9085205 docs: 创建P0和P1改进项实现方案
- IMPL-001: 响应式编程培训方案
- IMPL-002: 敏感数据加密存储方案
- IMPL-003: 预约高峰期性能优化方案
- IMPL-004: 支付接口幂等性校验方案
2026-04-05 16:48:27 +08:00

591 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<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:验证加密数据正确性**
```java
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:删除明文字段**
```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<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 单元测试
#### 加密/解密功能测试
```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)