SpringBoot+MyBatis實(shí)現(xiàn)數(shù)據(jù)庫(kù)字段級(jí)加密
前言
在數(shù)據(jù)安全越來(lái)越受重視的今天,如何保護(hù)用戶的敏感信息成為每個(gè)開(kāi)發(fā)者都要面對(duì)的問(wèn)題。比如用戶的手機(jī)號(hào)、身份證、銀行卡這些信息,如果直接存在數(shù)據(jù)庫(kù)里,一旦數(shù)據(jù)泄露,后果很嚴(yán)重。
傳統(tǒng)的做法是在每個(gè)查詢和插入的地方手動(dòng)加解密,但這樣做代碼會(huì)變得很亂,而且容易遺漏。今天分享一個(gè)基于注解的自動(dòng)加解密方案,通過(guò) Spring Boot + MyBatis 實(shí)現(xiàn),讓敏感字段自動(dòng)加密存儲(chǔ),自動(dòng)解密使用。
遇到的實(shí)際問(wèn)題
傳統(tǒng)加密方式的問(wèn)題
代碼重復(fù)太多
// 每個(gè)查詢都要手動(dòng)處理 User user = userMapper.findById(id); user.setPhone(decrypt(user.getPhone())); user.setEmail(decrypt(user.getEmail())); user.setIdCard(decrypt(user.getIdCard())); return user; // 插入時(shí)也要手動(dòng)加密 User newUser = new User(); newUser.setPhone(encrypt(phone)); newUser.setEmail(encrypt(email)); userMapper.insert(newUser);
修改很麻煩
- 新增加密字段要改很多地方
- 容易忘記某個(gè)查詢的加解密
- 代碼到處都是加解密邏輯
維護(hù)成本高
- 加密邏輯分散在各個(gè)方法里
- 出了問(wèn)題很難排查
- 新人接手要理解整套加密邏輯
我們需要什么樣的方案
理想中的方案應(yīng)該是:
- 在需要加密的字段上加個(gè)注解就行
- 加解密過(guò)程自動(dòng)完成
- 業(yè)務(wù)代碼不用關(guān)心加密邏輯
- 性能要好,不能影響正常業(yè)務(wù)
解決方案設(shè)計(jì)
核心思路
1. 注解標(biāo)記:用 @Encrypted 注解標(biāo)記要加密的字段
2. 攔截處理:MyBatis 攔截器自動(dòng)處理加解密
3. 透明操作:業(yè)務(wù)代碼感覺(jué)不到加密的存在
技術(shù)架構(gòu)
業(yè)務(wù)代碼 → MyBatis Mapper → 攔截器 → 自動(dòng)加解密 → 數(shù)據(jù)庫(kù)
簡(jiǎn)單來(lái)說(shuō),就是在業(yè)務(wù)層和數(shù)據(jù)庫(kù)之間加了一層透明的加解密處理。
核心實(shí)現(xiàn)
1. 定義注解
先定義一個(gè)簡(jiǎn)單的注解來(lái)標(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 // 這個(gè)字段會(huì)自動(dòng)加密
private String phone;
@Encrypted // 這個(gè)字段也會(huì)自動(dòng)加密
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) {
// 生成隨機(jī)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 攔截器
攔截器是整個(gè)方案的核心,負(fù)責(zé)自動(dòng)加解密:
@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;
// 只處理實(shí)體對(duì)象,不處理基本類型和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. 自動(dòng)配置
配置攔截器自動(dòng)生效:
@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ù)庫(kù)存儲(chǔ)情況
原始數(shù)據(jù):
用戶信息:
姓名: 張三
手機(jī): 13812345678
郵箱: zhangsan@example.com
身份證: 110101199001011234
數(shù)據(jù)庫(kù)存儲(chǔ)(自動(dòng)加密后):
用戶信息:
姓名: 張三
手機(jī): nTuVgMWime1:hFGa9as6JHxLT2vG8dpiRmu4wtxDnkTEr/1x
郵箱: mK7pL9xQ2rS8vN3w:jKxL9mN2pQ7rS8vT3wX4yZ6aB8cD1eF2g
身份證: X1Y2Z3A4B5C6D7E8:F9G0H1I2J3K4L5M6N7O8P9Q0R1S2T3U4V
業(yè)務(wù)代碼使用
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// 插入用戶:自動(dòng)加密存儲(chǔ)
public void createUser(User user) {
// 這些字段會(huì)被攔截器自動(dòng)加密
user.setPhone("13812345678");
user.setEmail("zhangsan@example.com");
userMapper.insert(user);
}
// 查詢用戶:自動(dòng)解密返回
public User getUser(Long id) {
User user = userMapper.findById(id);
// 這些字段已經(jīng)被攔截器自動(dòng)解密
System.out.println(user.getPhone()); // 13812345678
System.out.println(user.getEmail()); // zhangsan@example.com
return user;
}
}
可以看到,業(yè)務(wù)代碼完全不用關(guān)心加密和解密的過(guò)程,一切都在幕后自動(dòng)完成。
安全考慮
1. 密鑰管理
實(shí)際項(xiàng)目中不要把密鑰寫(xiě)死在代碼里:
@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é)
這個(gè)基于注解的字段級(jí)加密方案有以下幾個(gè)優(yōu)點(diǎn):
使用簡(jiǎn)單:只需要在字段上加個(gè)注解就行
代碼干凈:業(yè)務(wù)代碼不用關(guān)心加解密邏輯
安全可靠:使用標(biāo)準(zhǔn)的加密算法
容易維護(hù):所有加密邏輯集中管理
適用場(chǎng)景:
- 用戶管理系統(tǒng)
- 支付系統(tǒng)
- 醫(yī)療信息系統(tǒng)
- 任何需要保護(hù)敏感數(shù)據(jù)的系統(tǒng)
不適用場(chǎng)景:
- 對(duì)性能要求極高的系統(tǒng)
- 需要對(duì)加密字段進(jìn)行復(fù)雜查詢的場(chǎng)景
- 數(shù)據(jù)量特別大的系統(tǒng)
如果你也有保護(hù)敏感數(shù)據(jù)的需求,這個(gè)方案值得考慮。代碼量不大,但效果很明顯。
到此這篇關(guān)于SpringBoot+MyBatis實(shí)現(xiàn)數(shù)據(jù)庫(kù)字段級(jí)加密的文章就介紹到這了,更多相關(guān)SpringBoot MyBatis數(shù)據(jù)庫(kù)字段加密內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
IDEA錯(cuò)誤:找不到或無(wú)法加載主類的完美解決方法
使用IDEA開(kāi)始就一直在搭建java環(huán)境,許久沒(méi)有使用過(guò)java,剛開(kāi)始有些生疏,先建了一個(gè)最簡(jiǎn)單的類可是運(yùn)行的時(shí)候出現(xiàn)錯(cuò)誤:找不到或無(wú)法加載主類,下面這篇文章主要給大家介紹了關(guān)于IDEA錯(cuò)誤:找不到或無(wú)法加載主類的完美解決方法,需要的朋友可以參考下2022-07-07
SpringBoot+Spring Security+JWT實(shí)現(xiàn)RESTful Api權(quán)限控制的方法
這篇文章主要介紹了SpringBoot+Spring Security+JWT實(shí)現(xiàn)RESTful Api權(quán)限控制的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03
Java 數(shù)據(jù)結(jié)構(gòu)鏈表操作實(shí)現(xiàn)代碼
這篇文章主要介紹了Java 數(shù)據(jù)結(jié)構(gòu)鏈表操作的相關(guān)資料,并附實(shí)例代碼,需要的朋友可以參考下2016-10-10
SpringBoot+Mybatis使用Enum枚舉類型總是報(bào)錯(cuò)No enum constant&n
這篇文章主要介紹了SpringBoot+Mybatis使用Enum枚舉類型總是報(bào)錯(cuò)No enum constant XX問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12
關(guān)于springboot使用rocketmq?RocketMQMessageListener參數(shù)問(wèn)題
這篇文章主要介紹了springboot使用rocketmq?RocketMQMessageListener參數(shù)問(wèn)題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值需要的朋友可以參考下2022-11-11
深入解析Spring Bean初始化時(shí)和銷毀時(shí)的擴(kuò)展點(diǎn)
在Bean進(jìn)行初始化或者銷毀的時(shí)候,如果我們需要做一些操作,比如加載和銷毀一些資源或者執(zhí)行一些方法時(shí),那么就可以使用Spring提供的一些擴(kuò)展,今天主要分享初始化Bean時(shí)的三種方式和銷毀Bean時(shí)的三種方式,需要的朋友可以參考下2023-08-08
java基于poi-ooxml編寫(xiě)Excel導(dǎo)出的工具類
這篇文章主要為大家詳細(xì)介紹了java如何基于poi-ooxml編寫(xiě)一個(gè)Excel導(dǎo)出功能的工具類,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-08-08
Java中防止數(shù)據(jù)重復(fù)提交超簡(jiǎn)單的6種方法
在平時(shí)開(kāi)發(fā)中,如果網(wǎng)速比較慢的情況下,用戶提交表單后,發(fā)現(xiàn)服務(wù)器半天都沒(méi)有響應(yīng),那么用戶可能會(huì)以為是自己沒(méi)有提交表單,就會(huì)再點(diǎn)擊提交按鈕重復(fù)提交表單,這篇文章主要給大家介紹了關(guān)于Java中防止數(shù)據(jù)重復(fù)提交超簡(jiǎn)單的6種方法,需要的朋友可以參考下2021-11-11

