MySQL数据加密与安全实践
数据安全的重要性
在当今的信息时代,数据泄露事件频发,给企业和用户带来巨大损失。数据库作为敏感信息的集中存储地,其安全性至关重要。对用户的手机号、身份证号、银行卡号等敏感字段进行加密,能够有效防止数据泄露后的信息滥用。即使数据库被"拖库",攻击者获取的也只是加密后的密文,无法直接使用。
数据加密方案
应用层加密解密
最常用且最灵活的方案是在应用层对敏感数据进行加密处理:
java
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private EncryptionService encryptionService;
// 保存用户信息时加密
public void createUser(UserDTO userDTO) {
User user = new User();
user.setUsername(userDTO.getUsername());
// 加密手机号
String encryptedPhone = encryptionService.encrypt(userDTO.getPhone());
user.setPhone(encryptedPhone);
// 加密身份证号
String encryptedIdCard = encryptionService.encrypt(userDTO.getIdCard());
user.setIdCard(encryptedIdCard);
// 密码使用单向Hash(不可逆)
String hashedPassword = encryptionService.hash(userDTO.getPassword());
user.setPassword(hashedPassword);
userRepository.save(user);
}
// 查询时解密
public UserVO getUserById(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new NotFoundException("用户不存在"));
UserVO userVO = new UserVO();
userVO.setUserId(user.getUserId());
userVO.setUsername(user.getUsername());
// 解密手机号
String decryptedPhone = encryptionService.decrypt(user.getPhone());
userVO.setPhone(maskPhone(decryptedPhone)); // 脱敏显示
// 解密身份证号
String decryptedIdCard = encryptionService.decrypt(user.getIdCard());
userVO.setIdCard(maskIdCard(decryptedIdCard));
return userVO;
}
// 手机号脱敏:138****5678
private String maskPhone(String phone) {
if (phone == null || phone.length() != 11) {
return phone;
}
return phone.substring(0, 3) + "****" + phone.substring(7);
}
// 身份证脱敏:110***********1234
private String maskIdCard(String idCard) {
if (idCard == null || idCard.length() < 8) {
return idCard;
}
return idCard.substring(0, 3) + "***********" + idCard.substring(idCard.length() - 4);
}
}加密服务实现(使用AES对称加密):
java
@Service
public class EncryptionService {
// 密钥应从配置中心获取,不应硬编码
@Value("${encryption.secret-key}")
private String secretKey;
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES/ECB/PKCS5Padding";
public String encrypt(String plainText) {
try {
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), ALGORITHM);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
throw new EncryptionException("加密失败", e);
}
}
public String decrypt(String cipherText) {
try {
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), ALGORITHM);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decoded = Base64.getDecoder().decode(cipherText);
byte[] decrypted = cipher.doFinal(decoded);
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new EncryptionException("解密失败", e);
}
}
// 单向Hash用于密码(不可逆)
public String hash(String plainText) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(plainText.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hash);
} catch (Exception e) {
throw new EncryptionException("Hash失败", e);
}
}
}应用层加密架构:
mermaid
graph TB
A[用户输入明文] --> B[应用层加密]
B --> C[存储密文到数据库]
D[查询请求] --> E[从数据库读取密文]
E --> F[应用层解密]
F --> G[返回明文结果]
H[密码输入] --> I[单向Hash]
I --> J[存储Hash值]
K[密码验证] --> L[输入密码Hash]
L --> M[与存储的Hash对比]
M --> N{匹配?}
N -->|是| O[验证通过]
N -->|否| P[验证失败]
style B fill:#7ED321,color:#fff
style F fill:#7ED321,color:#fff
style I fill:#4A90E2,color:#fff数据库加密函数
MySQL提供了内置的加密函数,可以直接在SQL中使用:
AES加密解密
sql
-- 创建用户表
CREATE TABLE users_encrypted (
user_id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
phone_encrypted BLOB, -- 存储加密后的手机号
email_encrypted BLOB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 插入加密数据
INSERT INTO users_encrypted (username, phone_encrypted, email_encrypted)
VALUES (
'李明',
AES_ENCRYPT('13812345678', 'my_secret_key_2024'),
AES_ENCRYPT('liming@example.com', 'my_secret_key_2024')
);
-- 查询并解密
SELECT
user_id,
username,
CAST(AES_DECRYPT(phone_encrypted, 'my_secret_key_2024') AS CHAR) AS phone,
CAST(AES_DECRYPT(email_encrypted, 'my_secret_key_2024') AS CHAR) AS email
FROM users_encrypted
WHERE user_id = 1;MD5和SHA哈希函数
sql
-- 创建账户表
CREATE TABLE user_accounts (
account_id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash CHAR(64) NOT NULL, -- SHA-256产生64位十六进制字符串
salt CHAR(32) NOT NULL, -- 盐值
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 注册用户(加盐Hash)
SET @username = 'zhangsan';
SET @password = 'MyPassword123';
SET @salt = MD5(RAND()); -- 生成随机盐值
INSERT INTO user_accounts (username, password_hash, salt)
VALUES (
@username,
SHA2(CONCAT(@password, @salt), 256), -- 密码+盐值进行SHA-256
@salt
);
-- 验证密码
SET @input_username = 'zhangsan';
SET @input_password = 'MyPassword123';
SELECT
CASE
WHEN password_hash = SHA2(CONCAT(@input_password, salt), 256)
THEN '密码正确'
ELSE '密码错误'
END AS validation_result
FROM user_accounts
WHERE username = @input_username;加盐Hash机制:
mermaid
graph LR
A[原始密码] --> B[添加随机盐值]
B --> C[SHA-256哈希]
C --> D[存储Hash值和盐值]
E[登录密码] --> F[读取盐值]
F --> G[密码+盐值]
G --> H[SHA-256哈希]
H --> I[对比Hash值]
style C fill:#7ED321,color:#fff
style I fill:#4A90E2,color:#fffInnoDB静态数据加密
MySQL 5.7+支持对InnoDB表空间进行透明加密:
sql
-- 启用加密的表
CREATE TABLE sensitive_data (
data_id INT PRIMARY KEY AUTO_INCREMENT,
secret_info VARCHAR(200),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENCRYPTION='Y';
-- 为已存在的表启用加密
ALTER TABLE sensitive_data ENCRYPTION='Y';
-- 查看表的加密状态
SELECT
table_schema,
table_name,
create_options
FROM information_schema.tables
WHERE table_name = 'sensitive_data';配置keyring插件(密钥管理):
properties
# my.cnf配置
[mysqld]
early-plugin-load=keyring_file.so
keyring_file_data=/var/lib/mysql-keyring/keyring表空间加密特点:
- 数据在磁盘上以加密形式存储
- 查询时自动解密,对应用透明
- 保护备份文件和binlog
- 性能影响较小(约5-15%)
加密后的挑战
性能开销
加密解密操作会增加CPU负担:
java
// 性能测试示例
@Test
public void testEncryptionPerformance() {
int iterations = 10000;
String plainText = "13812345678";
// 测试加密性能
long startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
encryptionService.encrypt(plainText);
}
long encryptTime = System.currentTimeMillis() - startTime;
System.out.println("10000次加密耗时: " + encryptTime + "ms");
// 输出示例: 10000次加密耗时: 850ms
// 测试解密性能
String cipherText = encryptionService.encrypt(plainText);
startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
encryptionService.decrypt(cipherText);
}
long decryptTime = System.currentTimeMillis() - startTime;
System.out.println("10000次解密耗时: " + decryptTime + "ms");
// 输出示例: 10000次解密耗时: 920ms
}性能优化策略:
- 仅对必要的敏感字段加密
- 使用缓存减少重复解密
- 批量操作时考虑异步处理
密钥管理复杂度
密钥的安全存储和轮换是关键挑战:
java
@Configuration
public class KeyManagementConfig {
// 错误示范:硬编码密钥
// private static final String SECRET_KEY = "hardcoded_key"; // 严重安全隐患!
// 推荐方案1:从配置中心读取
@Value("${encryption.key}")
private String encryptionKey;
// 推荐方案2:从密钥管理服务获取
@Bean
public KeyManager keyManager() {
// 集成AWS KMS、Azure Key Vault或阿里云KMS
return new CloudKeyManager();
}
// 推荐方案3:使用环境变量
@Bean
public String getSecretKey() {
String key = System.getenv("DB_ENCRYPTION_KEY");
if (key == null) {
throw new IllegalStateException("加密密钥未配置");
}
return key;
}
}密钥轮换策略:
mermaid
graph TB
A[密钥版本1使用中] --> B[生成密钥版本2]
B --> C[新数据使用版本2加密]
C --> D[逐步迁移旧数据]
D --> E{所有数据已迁移?}
E -->|否| D
E -->|是| F[废弃密钥版本1]
style A fill:#F5A623,color:#fff
style C fill:#7ED321,color:#fff
style F fill:#4A90E2,color:#fff加密后的模糊查询
加密数据的模糊查询是一个技术难题,因为密文之间不存在明文的相似关系。
方案一:全表解密(不推荐)
java
// 性能极差的方案
public List<User> searchByPhoneLike(String phonePattern) {
List<User> allUsers = userRepository.findAll();
return allUsers.stream()
.filter(user -> {
String decryptedPhone = encryptionService.decrypt(user.getPhone());
return decryptedPhone.contains(phonePattern);
})
.collect(Collectors.toList());
}缺点:
- 需要加载所有数据到内存
- 解密开销巨大
- 容易导致OOM
- 无法利用索引
方案二:数据库解密函数查询
sql
-- 在WHERE中使用解密函数
SELECT
user_id,
username,
CAST(AES_DECRYPT(phone_encrypted, 'my_secret_key') AS CHAR) AS phone
FROM users_encrypted
WHERE CAST(AES_DECRYPT(phone_encrypted, 'my_secret_key') AS CHAR) LIKE '138%';缺点:
- 索引失效(函数作用于字段)
- 全表扫描
- 适用于小数据量场景
方案三:明文关键词分词索引(推荐)
将敏感数据的关键部分分词后加密存储:
sql
-- 优化后的用户表设计
CREATE TABLE users_searchable (
user_id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
phone_encrypted BLOB NOT NULL, -- 完整手机号加密
phone_search_tokens TEXT, -- 用于搜索的加密分词,例如:"enc_138,enc_381,enc_812,enc_123,enc_234,enc_345,enc_456,enc_567,enc_678"
email_encrypted BLOB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_search_tokens (phone_search_tokens(100)) -- 前缀索引
);Java实现:
java
@Service
public class SearchableEncryptionService {
@Autowired
private EncryptionService encryptionService;
// 生成搜索令牌
public String generateSearchTokens(String phone) {
Set<String> tokens = new HashSet<>();
// 生成各种长度的子串
for (int length = 3; length <= phone.length(); length++) {
for (int i = 0; i <= phone.length() - length; i++) {
String substring = phone.substring(i, i + length);
// 加密子串
String encryptedToken = encryptionService.encrypt(substring);
tokens.add(encryptedToken);
}
}
return String.join(",", tokens);
}
// 保存用户
public void saveUserWithSearch(String username, String phone, String email) {
User user = new User();
user.setUsername(username);
user.setPhoneEncrypted(encryptionService.encrypt(phone));
user.setEmailEncrypted(encryptionService.encrypt(email));
// 生成搜索令牌
String searchTokens = generateSearchTokens(phone);
user.setPhoneSearchTokens(searchTokens);
userRepository.save(user);
}
// 模糊搜索
public List<User> searchByPhonePattern(String pattern) {
// 加密搜索模式
String encryptedPattern = encryptionService.encrypt(pattern);
// 使用LIKE查询(可以利用索引前缀)
return userRepository.findByPhoneSearchTokensContaining(encryptedPattern);
}
}对应的SQL查询:
sql
-- 查询手机号包含"138"的用户
SET @search_pattern = AES_ENCRYPT('138', 'my_secret_key');
SELECT
user_id,
username,
CAST(AES_DECRYPT(phone_encrypted, 'my_secret_key') AS CHAR) AS phone
FROM users_searchable
WHERE phone_search_tokens LIKE CONCAT('%', @search_pattern, '%');分词索引方案架构:
mermaid
graph TB
A[明文: 13812345678] --> B[分词生成子串]
B --> C[138, 381, 812, 1381, 3812, ...]
C --> D[加密各个子串]
D --> E[存储加密令牌列表]
F[搜索: 138] --> G[加密搜索词]
G --> H[在令牌列表中查找]
H --> I[命中记录]
I --> J[解密返回完整数据]
style D fill:#7ED321,color:#fff
style H fill:#4A90E2,color:#fff方案四:布隆过滤器优化
对于大数据量场景,可使用布隆过滤器快速过滤:
java
@Service
public class BloomFilterSearchService {
private BloomFilter<String> phoneBloomFilter;
@PostConstruct
public void init() {
// 预估100万用户,误判率0.01
phoneBloomFilter = BloomFilter.create(
Funnels.stringFunnel(StandardCharsets.UTF_8),
1000000,
0.01
);
// 加载已有数据到布隆过滤器
List<User> users = userRepository.findAll();
for (User user : users) {
String phone = encryptionService.decrypt(user.getPhoneEncrypted());
phoneBloomFilter.put(phone);
}
}
public List<User> searchWithBloomFilter(String pattern) {
// 快速判断是否可能存在
if (!phoneBloomFilter.mightContain(pattern)) {
return Collections.emptyList(); // 一定不存在
}
// 可能存在,执行精确查询
String encryptedPattern = encryptionService.encrypt(pattern);
return userRepository.findByPhoneSearchTokensContaining(encryptedPattern);
}
}真实案例参考
淘宝的加密搜索方案
淘宝对收货人手机号采用分段加密索引:
sql
-- 简化版示例
CREATE TABLE delivery_addresses (
address_id BIGINT PRIMARY KEY,
receiver_name_encrypted VARCHAR(200),
phone_full_encrypted VARCHAR(200), -- 完整手机号加密
phone_prefix_encrypted CHAR(50), -- 前3位加密
phone_middle_encrypted CHAR(50), -- 中间4位加密
phone_suffix_encrypted CHAR(50), -- 后4位加密
INDEX idx_prefix (phone_prefix_encrypted),
INDEX idx_middle (phone_middle_encrypted),
INDEX idx_suffix (phone_suffix_encrypted)
);
-- 搜索优化:根据输入位置选择索引
-- 搜索"138****"时使用prefix索引
-- 搜索"***1234"时使用suffix索引数据脱敏最佳实践
除了加密,还应在展示层进行数据脱敏:
java
@Service
public class DataMaskingService {
// 手机号脱敏
public String maskPhone(String phone) {
if (StringUtils.isEmpty(phone) || phone.length() != 11) {
return phone;
}
return phone.substring(0, 3) + "****" + phone.substring(7);
}
// 身份证号脱敏
public String maskIdCard(String idCard) {
if (StringUtils.isEmpty(idCard)) {
return idCard;
}
int length = idCard.length();
if (length == 15) {
return idCard.substring(0, 6) + "*****" + idCard.substring(11);
} else if (length == 18) {
return idCard.substring(0, 6) + "********" + idCard.substring(14);
}
return idCard;
}
// 银行卡号脱敏
public String maskBankCard(String cardNo) {
if (StringUtils.isEmpty(cardNo) || cardNo.length() < 8) {
return cardNo;
}
return cardNo.substring(0, 4) + " **** **** " + cardNo.substring(cardNo.length() - 4);
}
// 邮箱脱敏
public String maskEmail(String email) {
if (StringUtils.isEmpty(email) || !email.contains("@")) {
return email;
}
String[] parts = email.split("@");
String username = parts[0];
if (username.length() <= 2) {
return "*@" + parts[1];
}
return username.substring(0, 2) + "***@" + parts[1];
}
}数据安全层次:
mermaid
graph TB
A[数据安全体系] --> B[存储层]
A --> C[传输层]
A --> D[展示层]
B --> B1[数据库加密]
B --> B2[文件加密]
B --> B3[备份加密]
C --> C1[HTTPS传输]
C --> C2[VPN专线]
D --> D1[数据脱敏]
D --> D2[权限控制]
D --> D3[操作审计]
style A fill:#4A90E2,color:#fff
style B fill:#7ED321,color:#fff
style C fill:#7ED321,color:#fff
style D fill:#7ED321,color:#fff通过多层次的安全防护,可以最大程度保障敏感数据的安全。加密方案的选择需要在安全性、性能和可用性之间找到平衡点。
更新: 2025-12-04 17:38:14
原文: https://www.yuque.com/u22210564/zoxfmt/doc-01-mysql-15-mysql-33