dec9085205
- IMPL-001: 响应式编程培训方案 - IMPL-002: 敏感数据加密存储方案 - IMPL-003: 预约高峰期性能优化方案 - IMPL-004: 支付接口幂等性校验方案
591 lines
14 KiB
Markdown
591 lines
14 KiB
Markdown
# 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)
|