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

14 KiB
Raw Blame History

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:解密失败

  • 应对:保存原始密文,提供手动解密工具

八、相关文档