SpringBoot+MyBatis實現(xiàn)數(shù)據(jù)庫字段級加密
前言
在數(shù)據(jù)安全越來越受重視的今天,如何保護(hù)用戶的敏感信息成為每個開發(fā)者都要面對的問題。比如用戶的手機號、身份證、銀行卡這些信息,如果直接存在數(shù)據(jù)庫里,一旦數(shù)據(jù)泄露,后果很嚴(yán)重。
傳統(tǒng)的做法是在每個查詢和插入的地方手動加解密,但這樣做代碼會變得很亂,而且容易遺漏。今天分享一個基于注解的自動加解密方案,通過 Spring Boot + MyBatis 實現(xiàn),讓敏感字段自動加密存儲,自動解密使用。
遇到的實際問題
傳統(tǒng)加密方式的問題
代碼重復(fù)太多
// 每個查詢都要手動處理 User user = userMapper.findById(id); user.setPhone(decrypt(user.getPhone())); user.setEmail(decrypt(user.getEmail())); user.setIdCard(decrypt(user.getIdCard())); return user; // 插入時也要手動加密 User newUser = new User(); newUser.setPhone(encrypt(phone)); newUser.setEmail(encrypt(email)); userMapper.insert(newUser);
修改很麻煩
- 新增加密字段要改很多地方
- 容易忘記某個查詢的加解密
- 代碼到處都是加解密邏輯
維護(hù)成本高
- 加密邏輯分散在各個方法里
- 出了問題很難排查
- 新人接手要理解整套加密邏輯
我們需要什么樣的方案
理想中的方案應(yīng)該是:
- 在需要加密的字段上加個注解就行
- 加解密過程自動完成
- 業(yè)務(wù)代碼不用關(guān)心加密邏輯
- 性能要好,不能影響正常業(yè)務(wù)
解決方案設(shè)計
核心思路
1. 注解標(biāo)記:用 @Encrypted 注解標(biāo)記要加密的字段
2. 攔截處理:MyBatis 攔截器自動處理加解密
3. 透明操作:業(yè)務(wù)代碼感覺不到加密的存在
技術(shù)架構(gòu)
業(yè)務(wù)代碼 → MyBatis Mapper → 攔截器 → 自動加解密 → 數(shù)據(jù)庫
簡單來說,就是在業(yè)務(wù)層和數(shù)據(jù)庫之間加了一層透明的加解密處理。
核心實現(xiàn)
1. 定義注解
先定義一個簡單的注解來標(biāo)記需要加密的字段:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Encrypted {
// 是否支持模糊查詢
boolean supportFuzzyQuery() default false;
}
使用方式:
public class User {
private Long id;
private String username;
@Encrypted // 這個字段會自動加密
private String phone;
@Encrypted // 這個字段也會自動加密
private String email;
}
2. 加密工具
使用 AES-GCM 算法進(jìn)行加密:
public class CryptoUtil {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int IV_LENGTH = 12;
public static String encrypt(String plaintext) {
// 生成隨機IV
byte[] iv = new byte[IV_LENGTH];
new SecureRandom().nextBytes(iv);
// 加密
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(128, iv));
byte[] ciphertext = cipher.doFinal(plaintext.getBytes());
// 組合IV和密文,Base64編碼
byte[] encryptedData = new byte[iv.length + ciphertext.length];
System.arraycopy(iv, 0, encryptedData, 0, iv.length);
System.arraycopy(ciphertext, 0, encryptedData, iv.length, ciphertext.length);
return Base64.getEncoder().encodeToString(encryptedData);
}
public static String decrypt(String encryptedText) {
// Base64解碼
byte[] encryptedData = Base64.getDecoder().decode(encryptedText);
// 提取IV和密文
byte[] iv = Arrays.copyOfRange(encryptedData, 0, IV_LENGTH);
byte[] ciphertext = Arrays.copyOfRange(encryptedData, IV_LENGTH, encryptedData.length);
// 解密
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, iv));
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext);
}
// 檢查是否已經(jīng)加密,避免重復(fù)加密
public static boolean isEncrypted(String value) {
return value != null && value.contains(":");
}
}
加密后的格式:Base64(IV):Base64(密文)
3. MyBatis 攔截器
攔截器是整個方案的核心,負(fù)責(zé)自動加解密:
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class EncryptionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
String methodName = invocation.getMethod().getName();
// UPDATE/INSERT 操作:加密輸入?yún)?shù)
if ("update".equals(methodName)) {
Object parameter = getParameter(invocation);
if (shouldEncrypt(parameter)) {
encryptFields(parameter);
}
}
// 執(zhí)行原始SQL
Object result = invocation.proceed();
// SELECT 操作:解密查詢結(jié)果
if ("query".equals(methodName)) {
decryptResult(result);
}
return result;
}
private void encryptFields(Object obj) {
if (obj == null) return;
// 只處理實體對象,不處理基本類型和Map
if (isBasicType(obj.getClass()) || obj instanceof Map || obj instanceof Collection) {
return;
}
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Encrypted.class)) {
try {
field.setAccessible(true);
Object value = field.get(obj);
if (value instanceof String && !isEncrypted((String) value)) {
String encrypted = CryptoUtil.encrypt((String) value);
field.set(obj, encrypted);
}
} catch (Exception e) {
log.error("加密字段失敗: {}", field.getName(), e);
}
}
}
}
private void decryptResult(Object result) {
if (result instanceof List) {
for (Object item : (List<?>) result) {
decryptFields(item);
}
} else if (result != null) {
decryptFields(result);
}
}
private void decryptFields(Object obj) {
// 解密邏輯和加密類似,但是反向操作
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Encrypted.class)) {
try {
field.setAccessible(true);
Object value = field.get(obj);
if (value instanceof String && isEncrypted((String) value)) {
String decrypted = CryptoUtil.decrypt((String) value);
field.set(obj, decrypted);
}
} catch (Exception e) {
log.error("解密字段失敗: {}", field.getName(), e);
}
}
}
}
}
4. 自動配置
配置攔截器自動生效:
@Configuration
@ConditionalOnProperty(name = "encryption.enabled", havingValue = "true", matchIfMissing = true)
public class EncryptionAutoConfiguration {
@Bean
public ConfigurationCustomizer encryptionConfigurationCustomizer() {
return configuration -> {
configuration.addInterceptor(new EncryptionInterceptor());
};
}
}
只需要在配置文件中啟用:
encryption: enabled: true
使用效果
數(shù)據(jù)庫存儲情況
原始數(shù)據(jù):
用戶信息:
姓名: 張三
手機: 13812345678
郵箱: zhangsan@example.com
身份證: 110101199001011234
數(shù)據(jù)庫存儲(自動加密后):
用戶信息:
姓名: 張三
手機: nTuVgMWime1:hFGa9as6JHxLT2vG8dpiRmu4wtxDnkTEr/1x
郵箱: mK7pL9xQ2rS8vN3w:jKxL9mN2pQ7rS8vT3wX4yZ6aB8cD1eF2g
身份證: X1Y2Z3A4B5C6D7E8:F9G0H1I2J3K4L5M6N7O8P9Q0R1S2T3U4V
業(yè)務(wù)代碼使用
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// 插入用戶:自動加密存儲
public void createUser(User user) {
// 這些字段會被攔截器自動加密
user.setPhone("13812345678");
user.setEmail("zhangsan@example.com");
userMapper.insert(user);
}
// 查詢用戶:自動解密返回
public User getUser(Long id) {
User user = userMapper.findById(id);
// 這些字段已經(jīng)被攔截器自動解密
System.out.println(user.getPhone()); // 13812345678
System.out.println(user.getEmail()); // zhangsan@example.com
return user;
}
}
可以看到,業(yè)務(wù)代碼完全不用關(guān)心加密和解密的過程,一切都在幕后自動完成。
安全考慮
1. 密鑰管理
實際項目中不要把密鑰寫死在代碼里:
@Configuration
public class EncryptionConfig {
@Value("${encryption.key}")
private String encryptionKey;
@Bean
public SecretKey getSecretKey() {
// 可以從環(huán)境變量、配置中心、密鑰管理系統(tǒng)獲取
byte[] keyBytes = Base64.getDecoder().decode(encryptionKey);
return new SecretKeySpec(keyBytes, "AES");
}
}
2. 日志安全
避免在日志中打印敏感信息:
public class SensitiveDataLogger {
public String maskPhone(String phone) {
if (phone == null || phone.length() != 11) return phone;
return phone.substring(0, 3) + "****" + phone.substring(7);
}
public String maskEmail(String email) {
if (email == null) return email;
int atIndex = email.indexOf("@");
if (atIndex <= 2) return "***" + email.substring(atIndex);
return email.substring(0, 2) + "***" + email.substring(atIndex);
}
}
總結(jié)
這個基于注解的字段級加密方案有以下幾個優(yōu)點:
使用簡單:只需要在字段上加個注解就行
代碼干凈:業(yè)務(wù)代碼不用關(guān)心加解密邏輯
安全可靠:使用標(biāo)準(zhǔn)的加密算法
容易維護(hù):所有加密邏輯集中管理
適用場景:
- 用戶管理系統(tǒng)
- 支付系統(tǒng)
- 醫(yī)療信息系統(tǒng)
- 任何需要保護(hù)敏感數(shù)據(jù)的系統(tǒng)
不適用場景:
- 對性能要求極高的系統(tǒng)
- 需要對加密字段進(jìn)行復(fù)雜查詢的場景
- 數(shù)據(jù)量特別大的系統(tǒng)
如果你也有保護(hù)敏感數(shù)據(jù)的需求,這個方案值得考慮。代碼量不大,但效果很明顯。
到此這篇關(guān)于SpringBoot+MyBatis實現(xiàn)數(shù)據(jù)庫字段級加密的文章就介紹到這了,更多相關(guān)SpringBoot MyBatis數(shù)據(jù)庫字段加密內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺談JSON的數(shù)據(jù)交換、緩存問題和同步問題
這篇文章主要介紹了淺談JSON的數(shù)據(jù)交換、緩存問題和同步問題,具有一定借鑒價值,需要的朋友可以參考下2017-12-12
Spring動態(tài)多數(shù)據(jù)源配置實例Demo
本篇文章主要介紹了Spring動態(tài)多數(shù)據(jù)源配置實例Demo,具有一定的參考價值,有興趣的可以了解一下。2017-01-01

