一、加密需求与场景
典型敏感数据
- 用户隐私:身份证号、手机号、邮箱、地址
- 支付信息:银行卡号、CVV、支付密码
- 医疗数据:病历、检查报告
- 商业机密:客户资料、合同金额
合规要求
- GDPR(欧盟):个人数据保护
- HIPAA(美国医疗):医疗数据安全
- PCI DSS(支付卡行业):持卡人数据保护
- 等保2.0(中国):三级以上要求敏感数据加密
二、加密方案分类
1. 应用层加密(推荐)
方案A:对称加密(AES)
@Component
public class AESEncryptor {
private static final String ALGORITHM = "AES/CBC/PKCS5Padding";
private static final String KEY_ALGORITHM = "AES";
@Value("${encryption.secret.key}")
private String secretKey; // 从配置中心读取,不硬编码
@Value("${encryption.iv}")
private String iv; // 初始化向量
/**
* 加密
*/
public String encrypt(String plainText) {
try {
SecretKeySpec keySpec = new SecretKeySpec(
secretKey.getBytes(StandardCharsets.UTF_8),
KEY_ALGORITHM
);
IvParameterSpec ivSpec = new IvParameterSpec(
iv.getBytes(StandardCharsets.UTF_8)
);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
byte[] encrypted = cipher.doFinal(
plainText.getBytes(StandardCharsets.UTF_8)
);
// Base64编码,方便存储
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
throw new RuntimeException("加密失败", e);
}
}
/**
* 解密
*/
public String decrypt(String encryptedText) {
try {
SecretKeySpec keySpec = new SecretKeySpec(
secretKey.getBytes(StandardCharsets.UTF_8),
KEY_ALGORITHM
);
IvParameterSpec ivSpec = new IvParameterSpec(
iv.getBytes(StandardCharsets.UTF_8)
);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
byte[] decoded = Base64.getDecoder().decode(encryptedText);
byte[] decrypted = cipher.doFinal(decoded);
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("解密失败", e);
}
}
}
使用示例:
@Service
public class UserService {
@Autowired
private AESEncryptor encryptor;
@Autowired
private JdbcTemplate jdbcTemplate;
public void saveUser(User user) {
// 加密敏感字段
String encryptedIdCard = encryptor.encrypt(user.getIdCard());
String encryptedPhone = encryptor.encrypt(user.getPhone());
jdbcTemplate.update(
"INSERT INTO users (name, id_card, phone) VALUES (?, ?, ?)",
user.getName(), encryptedIdCard, encryptedPhone
);
}
public User getUser(Long id) {
Map<String, Object> row = jdbcTemplate.queryForMap(
"SELECT * FROM users WHERE id = ?", id
);
User user = new User();
user.setId((Long) row.get("id"));
user.setName((String) row.get("name"));
// 解密敏感字段
user.setIdCard(encryptor.decrypt((String) row.get("id_card")));
user.setPhone(encryptor.decrypt((String) row.get("phone")));
return user;
}
}
表结构:
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
id_card VARCHAR(500), -- 加密后长度增加,需预留空间
phone VARCHAR(500),
create_time DATETIME
);
优势:
- ✅ 灵活性高,业务完全可控
- ✅ 支持密钥轮换
- ✅ 可选择性加密字段
- ✅ 跨数据库兼容
劣势:
- ❌ 加密后无法使用索引查询
- ❌ 无法使用LIKE模糊查询
- ❌ 应用层性能开销
方案B:非对称加密(RSA)
@Component
public class RSAEncryptor {
private PrivateKey privateKey;
private PublicKey publicKey;
@PostConstruct
public void init() throws Exception {
// 从配置中心或密钥管理服务获取密钥对
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
KeyPair keyPair = keyGen.generateKeyPair();
this.privateKey = keyPair.getPrivate();
this.publicKey = keyPair.getPublic();
}
/**
* 公钥加密(前端/客户端使用)
*/
public String encryptByPublicKey(String plainText) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encrypted = cipher.doFinal(
plainText.getBytes(StandardCharsets.UTF_8)
);
return Base64.getEncoder().encodeToString(encrypted);
}
/**
* 私钥解密(后端使用)
*/
public String decryptByPrivateKey(String encryptedText) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decoded = Base64.getDecoder().decode(encryptedText);
byte[] decrypted = cipher.doFinal(decoded);
return new String(decrypted, StandardCharsets.UTF_8);
}
}
使用场景:
- 客户端提交敏感数据时加密传输
- 后端解密后再用AES加密存储
注意:RSA加密性能较差,不适合大量数据加密。
2. 数据库内置加密函数
MySQL自带加密函数
-- AES加密
INSERT INTO users (name, id_card)
VALUES ('张三', AES_ENCRYPT('110101199001011234', 'secret_key_123'));
-- AES解密
SELECT
id,
name,
CAST(AES_DECRYPT(id_card, 'secret_key_123') AS CHAR) AS id_card
FROM users
WHERE id = 1;
问题:
- ❌ 密钥硬编码在SQL中(安全风险)
- ❌ 每次查询都需要调用解密函数(性能差)
- ❌ 密钥轮换困难
改进方案:结合用户变量
SET @key = 'secret_key_from_app';
SELECT
id,
name,
CAST(AES_DECRYPT(id_card, @key) AS CHAR) AS id_card
FROM users;
3. 透明数据加密(TDE)
MySQL TDE配置
-- 1. 启用TDE(需要MySQL企业版或Percona/MariaDB)
-- 编辑 my.cnf
[mysqld]
early-plugin-load=keyring_file.so
keyring_file_data=/var/lib/mysql-keyring/keyring
-- 2. 创建加密表空间
CREATE TABLESPACE encrypted_space
ADD DATAFILE 'encrypted_space.ibd'
ENCRYPTION='Y';
-- 3. 创建加密表
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
id_card VARCHAR(100),
phone VARCHAR(50)
) TABLESPACE encrypted_space
ENCRYPTION='Y';
-- 或对已有表开启加密
ALTER TABLE users ENCRYPTION='Y';
TDE原理:
数据写入流程:
用户数据 → InnoDB Buffer Pool → 加密(Master Key + Table Key)→ 写入磁盘
数据读取流程:
磁盘数据 → 解密 → InnoDB Buffer Pool → 返回明文给应用
优势:
- ✅ 对应用透明,无需改代码
- ✅ 保护磁盘数据(防止物理盗取)
- ✅ 支持索引查询
- ✅ 性能损耗小(5-10%)
劣势:
- ❌ 需要企业版或特定发行版
- ❌ 只保护静态数据(data-at-rest),不保护传输和内存
- ❌ 数据库备份文件需单独加密
4. 列级加密(MySQL 8.0企业版)
-- 创建加密列
CREATE TABLE credit_cards (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT,
card_number VARBINARY(500), -- 存储加密数据
KEY idx_user (user_id)
);
-- 插入时加密
INSERT INTO credit_cards (user_id, card_number)
VALUES (1001, AES_ENCRYPT('6222021234567890', UNHEX(SHA2('master_key', 256))));
-- 查询时解密
SELECT
id,
user_id,
CAST(AES_DECRYPT(card_number, UNHEX(SHA2('master_key', 256))) AS CHAR) AS card_number
FROM credit_cards
WHERE user_id = 1001;
5. 应用层+MyBatis拦截器(自动化)
@Intercepts({
@Signature(
type = ParameterHandler.class,
method = "setParameters",
args = {PreparedStatement.class}
)
})
public class EncryptionInterceptor implements Interceptor {
@Autowired
private AESEncryptor encryptor;
@Override
public Object intercept(Invocation invocation) throws Throwable {
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
// 获取参数对象
Object parameterObject = parameterHandler.getParameterObject();
if (parameterObject != null) {
// 反射处理带@Encrypted注解的字段
Class<?> clazz = parameterObject.getClass();
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Encrypted.class)) {
field.setAccessible(true);
Object value = field.get(parameterObject);
if (value instanceof String) {
String encrypted = encryptor.encrypt((String) value);
field.set(parameterObject, encrypted);
}
}
}
}
return invocation.proceed();
}
}
// 自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Encrypted {
}
实体类使用:
@Data
public class User {
private Long id;
private String name;
@Encrypted // 自动加密
private String idCard;
@Encrypted // 自动加密
private String phone;
}
三、密钥管理
1. 密钥存储方案
方案A:配置中心(Apollo/Nacos)
# Apollo配置
encryption:
aes:
key: ${AES_SECRET_KEY} # 占位符,实际值在配置中心
iv: ${AES_IV}
安全措施:
- 配置中心开启访问控制
- 密钥配置设置为”敏感信息”不可见
- 审计密钥访问日志
方案B:专业密钥管理服务(KMS)
@Component
public class KMSEncryptor {
@Autowired
private AWSKMSClient kmsClient;
@Value("${kms.key.id}")
private String keyId;
public String encrypt(String plainText) {
EncryptRequest request = new EncryptRequest()
.withKeyId(keyId)
.withPlaintext(ByteBuffer.wrap(plainText.getBytes()));
EncryptResult result = kmsClient.encrypt(request);
return Base64.getEncoder().encodeToString(
result.getCiphertextBlob().array()
);
}
public String decrypt(String encryptedText) {
DecryptRequest request = new DecryptRequest()
.withCiphertextBlob(ByteBuffer.wrap(
Base64.getDecoder().decode(encryptedText)
));
DecryptResult result = kmsClient.decrypt(request);
return new String(result.getPlaintext().array());
}
}
支持的KMS服务:
- AWS KMS
- Azure Key Vault
- 阿里云KMS
- 腾讯云KMS
- HashiCorp Vault
2. 密钥轮换策略
@Component
public class KeyRotationManager {
private static final String CURRENT_KEY_VERSION = "v2";
@Autowired
private Map<String, AESEncryptor> encryptorMap; // key版本 -> 加密器
/**
* 加密时始终使用最新密钥
*/
public String encrypt(String plainText) {
AESEncryptor currentEncryptor = encryptorMap.get(CURRENT_KEY_VERSION);
return CURRENT_KEY_VERSION + ":" + currentEncryptor.encrypt(plainText);
}
/**
* 解密时根据版本号选择密钥
*/
public String decrypt(String encryptedText) {
String[] parts = encryptedText.split(":", 2);
String version = parts[0];
String cipherText = parts[1];
AESEncryptor encryptor = encryptorMap.get(version);
if (encryptor == null) {
throw new RuntimeException("密钥版本不存在: " + version);
}
return encryptor.decrypt(cipherText);
}
/**
* 批量重新加密(密钥轮换后)
*/
@Scheduled(cron = "0 0 3 * * ?")
public void reEncryptOldData() {
Long lastId = 0L;
int batchSize = 1000;
while (true) {
List<User> users = jdbcTemplate.query(
"SELECT * FROM users WHERE id > ? AND id_card LIKE 'v1:%' LIMIT ?",
new UserRowMapper(), lastId, batchSize
);
if (users.isEmpty()) {
break;
}
for (User user : users) {
// 用旧密钥解密
String plainText = decryptWithOldKey(user.getIdCard());
// 用新密钥加密
String newEncrypted = encrypt(plainText);
// 更新数据库
jdbcTemplate.update(
"UPDATE users SET id_card = ? WHERE id = ?",
newEncrypted, user.getId()
);
}
lastId = users.get(users.size() - 1).getId();
}
}
}
四、性能优化
1. 批量加解密
public List<User> getUsersBatch(List<Long> ids) {
// 1. 批量查询
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
"SELECT * FROM users WHERE id IN (" +
ids.stream().map(String::valueOf).collect(Collectors.joining(",")) + ")"
);
// 2. 批量解密(并行处理)
return rows.parallelStream()
.map(row -> {
User user = new User();
user.setId((Long) row.get("id"));
user.setName((String) row.get("name"));
user.setIdCard(encryptor.decrypt((String) row.get("id_card")));
user.setPhone(encryptor.decrypt((String) row.get("phone")));
return user;
})
.collect(Collectors.toList());
}
2. 分级加密
// 不同敏感级别使用不同策略
public class SensitiveDataHandler {
// 高敏感:银行卡号 → 强加密(RSA)
public String encryptHighSensitive(String data) {
return rsaEncryptor.encrypt(data);
}
// 中敏感:手机号 → AES加密
public String encryptMediumSensitive(String data) {
return aesEncryptor.encrypt(data);
}
// 低敏感:地址 → 仅脱敏显示,不加密存储
public String maskLowSensitive(String data) {
return data.substring(0, 5) + "****";
}
}
3. 缓存解密结果
@Service
public class UserServiceWithCache {
@Cacheable(value = "user_cache", key = "#id")
public User getUserDecrypted(Long id) {
// 解密后的结果缓存到Redis(注意设置过期时间)
Map<String, Object> row = jdbcTemplate.queryForMap(
"SELECT * FROM users WHERE id = ?", id
);
User user = new User();
user.setId(id);
user.setIdCard(encryptor.decrypt((String) row.get("id_card")));
// ...
return user;
}
}
注意:缓存敏感数据需评估安全风险。
五、答题总结
面试回答框架:
-
方案选择:
“生产环境推荐应用层加密,使用AES对称加密,密钥存储在配置中心或KMS服务。对性能要求高的场景可选择MySQL TDE透明加密,但需要企业版” -
实现细节:
“应用层在插入前加密、查询后解密,使用AES/CBC/PKCS5Padding模式,密文Base64编码存储。表字段长度需预留(如VARCHAR(500)),因为加密后长度增加” -
密钥管理:
“密钥不能硬编码,应存储在配置中心并限制访问权限。支持密钥轮换,加密时带版本号(如v2:密文),解密时根据版本选择对应密钥” -
注意事项:
“加密字段无法建索引查询,需要额外设计;LIKE模糊查询不支持,需要特殊方案;高并发场景要注意加解密性能开销,可使用批量处理和并行解密优化”
关键点:
- 理解应用层加密 vs 数据库TDE的差异
- 掌握AES加密的完整流程
- 强调密钥管理的重要性
- 知道加密对查询和性能的影响