扔掉工具類(lèi)!MyBatis 一個(gè)簡(jiǎn)單配置搞定加密、解密,好用!
兄弟們,在 Java 開(kāi)發(fā)的江湖里,有一個(gè)流傳已久的傳說(shuō):每個(gè)程序員的電腦里都藏著一個(gè)神秘的 EncryptUtils 工具類(lèi)。這個(gè)類(lèi)里往往塞滿(mǎn)了各種加密解密的代碼,比如 AES、DES、MD5 等算法的實(shí)現(xiàn)。每次開(kāi)發(fā)新功能時(shí),我們都要像供祖宗一樣把它請(qǐng)出來(lái),在代碼里到處調(diào)用它的 encrypt() 和 decrypt() 方法。
但是,這樣的日子真的要持續(xù)下去嗎?想象一下,你的代碼里到處都是 EncryptUtils.encrypt(user.getPhone()) 這樣的調(diào)用,就像在整潔的白襯衫上灑了一堆咖啡漬。更要命的是,當(dāng)需求變更時(shí),比如要把 AES 換成 SM4 算法,你得像掃雷一樣在代碼里一個(gè)一個(gè)地改這些調(diào)用。這不是編程,這是在給代碼 “上刑” 啊!
今天,我就要給大家介紹一個(gè) “偷懶神器”——MyBatis 的加密解密配置。有了它,你可以徹底扔掉那個(gè)笨重的工具類(lèi),讓 MyBatis 自動(dòng)幫你搞定加密解密,從此告別手動(dòng)搬磚的日子!
一、傳統(tǒng)加密方式的 “七宗罪”
在正式介紹 MyBatis 的解決方案之前,我們先來(lái)吐槽一下傳統(tǒng)加密方式的種種 “罪行”。
1. 代碼冗余到讓人頭禿
假設(shè)你有一個(gè)用戶(hù)實(shí)體類(lèi) User,里面有 phone、idCard、email 三個(gè)敏感字段需要加密。按照傳統(tǒng)方式,你得在每個(gè) set 方法里調(diào)用加密工具類(lèi):
public void setPhone(String phone) {
this.phone = EncryptUtils.encrypt(phone);
}
public void setIdCard(String idCard) {
this.idCard = EncryptUtils.encrypt(idCard);
}
public void setEmail(String email) {
this.email = EncryptUtils.encrypt(email);
}這還不算完,在查詢(xún)的時(shí)候,你還得在每個(gè) get 方法里調(diào)用解密方法。要是有 10 個(gè)這樣的實(shí)體類(lèi),每個(gè)類(lèi)有 5 個(gè)敏感字段,你就得寫(xiě) 100 次加密解密的代碼。這不是編程,這是在寫(xiě) “代碼八股文” 啊!
2. 維護(hù)成本高得離譜
有一天,產(chǎn)品經(jīng)理突然跑過(guò)來(lái)跟你說(shuō):“我們要把加密算法從 AES 換成 SM4?!?你心里咯噔一下,知道噩夢(mèng)來(lái)了。你得打開(kāi)每個(gè)實(shí)體類(lèi),把所有的 EncryptUtils.encrypt 換成 SM4EncryptUtils.encrypt。要是漏掉一個(gè)地方,后果不堪設(shè)想。更可怕的是,你還得檢查所有的 get 方法,確保解密方法也換成了對(duì)應(yīng)的 SM4 實(shí)現(xiàn)。這簡(jiǎn)直就是一場(chǎng) “代碼馬拉松”!
3. 安全隱患防不勝防
假設(shè)你的工具類(lèi)里有一個(gè) bug,比如加密后的字符串沒(méi)有進(jìn)行 Base64 編碼,直接存到數(shù)據(jù)庫(kù)里了。這時(shí)候,數(shù)據(jù)庫(kù)里就會(huì)出現(xiàn)一堆亂碼。更要命的是,這個(gè) bug 可能在測(cè)試環(huán)境里沒(méi)有被發(fā)現(xiàn),直到上線(xiàn)后才暴露出來(lái)。這時(shí)候,你就得像救火隊(duì)員一樣緊急修復(fù),還要處理已經(jīng)存到數(shù)據(jù)庫(kù)里的亂碼數(shù)據(jù)。這不是在編程,這是在玩火啊!
二、MyBatis 的 “黑魔法”:自定義類(lèi)型處理器
既然傳統(tǒng)方式這么坑爹,那 MyBatis 有什么妙招呢?答案就是 —— 自定義類(lèi)型處理器(TypeHandler)。這個(gè)家伙就像一個(gè)隱形的管家,會(huì)在數(shù)據(jù)存入數(shù)據(jù)庫(kù)之前自動(dòng)加密,在取出數(shù)據(jù)的時(shí)候自動(dòng)解密。
1. 什么是類(lèi)型處理器?
簡(jiǎn)單來(lái)說(shuō),類(lèi)型處理器就是 MyBatis 用來(lái)處理 Java 類(lèi)型和數(shù)據(jù)庫(kù)類(lèi)型之間轉(zhuǎn)換的工具。比如,當(dāng)你把一個(gè) Java 的 String 類(lèi)型存入數(shù)據(jù)庫(kù)的 VARCHAR 字段時(shí),MyBatis 會(huì)通過(guò)類(lèi)型處理器進(jìn)行轉(zhuǎn)換。默認(rèn)情況下,MyBatis 已經(jīng)內(nèi)置了很多類(lèi)型處理器,比如處理 Integer、Date 等基本類(lèi)型的。
但是,對(duì)于敏感數(shù)據(jù)的加密解密,我們需要自定義類(lèi)型處理器。這樣,MyBatis 就會(huì)在處理這些敏感字段的時(shí)候,自動(dòng)調(diào)用我們自定義的加密解密邏輯。
2. 動(dòng)手寫(xiě)一個(gè)加密解密的類(lèi)型處理器
第一步:定義加密解密工具類(lèi)
雖然我們要扔掉工具類(lèi),但這里還是需要一個(gè)簡(jiǎn)單的工具類(lèi)來(lái)實(shí)現(xiàn)加密解密邏輯。不過(guò)別擔(dān)心,這個(gè)工具類(lèi)只會(huì)在類(lèi)型處理器里使用,不會(huì)污染業(yè)務(wù)代碼。
public class EncryptUtil {
private static final String KEY = "mySecretKey123456"; // 這里要換成你的實(shí)際密鑰
public static String encrypt(String data) {
// 實(shí)現(xiàn)加密邏輯,比如 AES 加密
return AES.encrypt(data, KEY);
}
public static String decrypt(String encryptedData) {
// 實(shí)現(xiàn)解密邏輯,比如 AES 解密
return AES.decrypt(encryptedData, KEY);
}
}這里需要注意,密鑰 KEY 千萬(wàn)不要硬編碼在代碼里,應(yīng)該通過(guò)配置文件或者環(huán)境變量來(lái)獲取。否則,你的代碼就像光著身子在大街上裸奔,毫無(wú)安全性可言!
第二步:創(chuàng)建自定義類(lèi)型處理器
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(String.class)
public class EncryptTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
// 在設(shè)置參數(shù)時(shí)進(jìn)行加密
String encryptedValue = EncryptUtil.encrypt(parameter);
ps.setString(i, encryptedValue);
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
// 從結(jié)果集中獲取數(shù)據(jù)時(shí)進(jìn)行解密
String encryptedValue = rs.getString(columnName);
return EncryptUtil.decrypt(encryptedValue);
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String encryptedValue = rs.getString(columnIndex);
return EncryptUtil.decrypt(encryptedValue);
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String encryptedValue = cs.getString(columnIndex);
return EncryptUtil.decrypt(encryptedValue);
}
}這個(gè)類(lèi)型處理器繼承自 BaseTypeHandler,并重寫(xiě)了四個(gè)方法:
- setNonNullParameter:在設(shè)置 SQL 參數(shù)時(shí)調(diào)用,用于加密數(shù)據(jù)。
- getNullableResult:在從結(jié)果集獲取數(shù)據(jù)時(shí)調(diào)用,用于解密數(shù)據(jù)。
這里的 @MappedJdbcTypes 和 @MappedTypes 注解指定了這個(gè)類(lèi)型處理器處理的數(shù)據(jù)庫(kù)類(lèi)型和 Java 類(lèi)型。比如,這里處理的是 VARCHAR 類(lèi)型的數(shù)據(jù)庫(kù)字段和 String 類(lèi)型的 Java 對(duì)象。
第三步:在 MyBatis 中注冊(cè)類(lèi)型處理器
在 MyBatis 的配置文件 mybatis-config.xml 中添加以下配置:
<typeHandlers>
<typeHandler handler="com.example.handler.EncryptTypeHandler"/>
</typeHandlers>或者,如果你使用的是 Spring Boot,可以在 application.properties 中配置:
mybatis.type-handlers-package=com.example.handler這樣,MyBatis 就知道在處理 VARCHAR 類(lèi)型的字段時(shí),要使用我們自定義的 EncryptTypeHandler 了。
3. 在實(shí)體類(lèi)中使用自定義類(lèi)型處理器
現(xiàn)在,我們可以在實(shí)體類(lèi)中使用這個(gè)類(lèi)型處理器了。比如,我們的 User 實(shí)體類(lèi)可以這樣寫(xiě):
public class User {
private Long id;
private String username;
@TypeHandler(EncryptTypeHandler.class)
private String phone;
@TypeHandler(EncryptTypeHandler.class)
private String idCard;
@TypeHandler(EncryptTypeHandler.class)
private String email;
// 省略 getter 和 setter 方法
}通過(guò) @TypeHandler 注解,我們告訴 MyBatis,phone、idCard、email 這三個(gè)字段需要使用 EncryptTypeHandler 進(jìn)行處理。這樣,當(dāng)我們插入或查詢(xún)用戶(hù)數(shù)據(jù)時(shí),MyBatis 會(huì)自動(dòng)幫我們加密和解密這些字段。
4. 測(cè)試一下效果
@Test
public void testEncryptAndDecrypt() {
User user = new User();
user.setPhone("13812345678");
user.setIdCard("123456789012345678");
user.setEmail("test@example.com");
// 插入數(shù)據(jù)
userMapper.insert(user);
// 查詢(xún)數(shù)據(jù)
User queriedUser = userMapper.selectById(user.getId());
// 斷言解密后的數(shù)據(jù)是否正確
assertEquals("13812345678", queriedUser.getPhone());
assertEquals("123456789012345678", queriedUser.getIdCard());
assertEquals("test@example.com", queriedUser.getEmail());
}運(yùn)行這個(gè)測(cè)試用例,如果所有斷言都通過(guò),那就說(shuō)明我們的自定義類(lèi)型處理器生效了!
三、攔截器:讓加密解密更 “絲滑”
雖然自定義類(lèi)型處理器已經(jīng)能解決大部分問(wèn)題,但它有一個(gè)小缺點(diǎn):每個(gè)需要加密的字段都得手動(dòng)添加 @TypeHandler 注解。如果你的項(xiàng)目中有成百上千個(gè)這樣的字段,這簡(jiǎn)直就是一場(chǎng)災(zāi)難!
這時(shí)候,MyBatis 的攔截器(Interceptor)就派上用場(chǎng)了。攔截器可以在 SQL 執(zhí)行的前后進(jìn)行攔截,對(duì)參數(shù)和結(jié)果進(jìn)行統(tǒng)一處理。這樣,我們就可以通過(guò)注解來(lái)標(biāo)記需要加密的字段,而不需要在每個(gè)字段上都添加 @TypeHandler 注解了。
1. 自定義加密注解
首先,我們需要定義一個(gè)注解,用來(lái)標(biāo)記需要加密的字段:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Encrypt {
}這個(gè)注解可以標(biāo)記在實(shí)體類(lèi)的字段上,表示該字段需要加密。
2. 創(chuà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})
})
publicclass EncryptInterceptor implements Interceptor {
@Override
publicObject intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
// 處理更新操作時(shí)的參數(shù)加密
if (invocation.getMethod().getName().equals("update")) {
encryptParameter(parameter);
}
// 執(zhí)行原始操作
Object result = invocation.proceed();
// 處理查詢(xún)操作時(shí)的結(jié)果解密
if (invocation.getMethod().getName().equals("query")) {
decryptResult(result);
}
return result;
}
privatevoid encryptParameter(Object parameter) {
if (parameter == null) {
return;
}
if (parameter instanceof Collection) {
for (Object obj : (Collection<?>) parameter) {
processObject(obj);
}
} elseif (parameter instanceof Map) {
for (Object value : ((Map<?, ?>) parameter).values()) {
processObject(value);
}
} else {
processObject(parameter);
}
}
privatevoid decryptResult(Object result) {
if (result == null) {
return;
}
if (result instanceof List) {
for (Object obj : (List<?>) result) {
processObject(obj);
}
} else {
processObject(result);
}
}
privatevoid processObject(Object obj) {
if (obj == null) {
return;
}
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Encrypt.class)) {
field.setAccessible(true);
try {
String value = (String) field.get(obj);
if (value != null) {
// 加密或解密操作
if (invocation.getMethod().getName().equals("update")) {
String encryptedValue = EncryptUtil.encrypt(value);
field.set(obj, encryptedValue);
} else {
String decryptedValue = EncryptUtil.decrypt(value);
field.set(obj, decryptedValue);
}
}
} catch (IllegalAccessException e) {
thrownew RuntimeException("處理加密字段失敗", e);
}
}
}
}
@Override
publicObject plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
publicvoid setProperties(Properties properties) {
// 可以在這里讀取配置文件中的參數(shù)
}
}這個(gè)攔截器的核心邏輯是:
- 在 update 方法執(zhí)行前,對(duì)參數(shù)中的 Encrypt 注解標(biāo)記的字段進(jìn)行加密。
- 在 query 方法執(zhí)行后,對(duì)結(jié)果中的 Encrypt 注解標(biāo)記的字段進(jìn)行解密。
3. 在 MyBatis 中注冊(cè)攔截器
同樣,在 MyBatis 的配置文件中注冊(cè)攔截器:
<plugins>
<plugin interceptor="com.example.interceptor.EncryptInterceptor"/>
</plugins>或者在 Spring Boot 中通過(guò) @Configuration 類(lèi)注冊(cè):
@Configuration
public class MyBatisConfig {
@Bean
public EncryptInterceptor encryptInterceptor() {
return new EncryptInterceptor();
}
}4. 在實(shí)體類(lèi)中使用注解
現(xiàn)在,我們的實(shí)體類(lèi)可以這樣寫(xiě):
public class User {
private Long id;
privateString username;
@Encrypt
privateString phone;
@Encrypt
privateString idCard;
@Encrypt
privateString email;
// 省略 getter 和 setter 方法
}只需要在需要加密的字段上添加 @Encrypt 注解,攔截器就會(huì)自動(dòng)處理加密和解密,再也不用手動(dòng)添加 @TypeHandler 注解了!
四、MyBatis-Plus 的 “懶人模式”
如果你使用的是 MyBatis-Plus,那事情就更簡(jiǎn)單了。MyBatis-Plus 從 3.3.2 版本開(kāi)始,內(nèi)置了數(shù)據(jù)安全保護(hù)功能,支持字段加密和解密。
1. 添加依賴(lài)
首先,在 pom.xml 中添加 MyBatis-Plus 的依賴(lài):
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.10</version>
</dependency>2. 配置加密
在 application.properties 中添加以下配置:
# 開(kāi)啟 MyBatis-Plus 的字段加密功能
mybatis-plus.global-config.encrypt.enable=true
# 配置加密密鑰(不要硬編碼,通過(guò)配置中心或環(huán)境變量獲?。?mybatis-plus.global-config.encrypt.key=mySecretKey123456
# 配置需要加密的字段
mybatis-plus.global-config.encrypt.fields=phone,idCard,email這里的 fields 屬性指定了需要加密的字段,多個(gè)字段用逗號(hào)分隔。
3. 在實(shí)體類(lèi)中使用注解
@Data
@TableName("user")
publicclass User {
private Long id;
privateString username;
@TableField(encrypt = true)
privateString phone;
@TableField(encrypt = true)
privateString idCard;
@TableField(encrypt = true)
privateString email;
}只需要在需要加密的字段上添加 @TableField(encrypt = true) 注解,MyBatis-Plus 就會(huì)自動(dòng)幫你處理加密和解密。
4. 進(jìn)階配置:自定義加密算法
如果你不滿(mǎn)足于默認(rèn)的 AES 算法,還可以自定義加密算法。首先,創(chuàng)建一個(gè)實(shí)現(xiàn) EncryptService 接口的類(lèi):
public class CustomEncryptService implements EncryptService {
privatestatic final String KEY = "mySecretKey123456";
@Override
publicString encrypt(String value) {
// 實(shí)現(xiàn)自定義加密邏輯
return SM4.encrypt(value, KEY);
}
@Override
publicString decrypt(String value) {
// 實(shí)現(xiàn)自定義解密邏輯
return SM4.decrypt(value, KEY);
}
}然后,在配置文件中指定使用自定義的加密服務(wù):
mybatis-plus.global-config.encrypt.service=com.example.service.CustomEncryptService這樣,MyBatis-Plus 就會(huì)使用你自定義的加密算法了。
五、性能優(yōu)化:讓加密解密飛起來(lái)
雖然加密解密能保證數(shù)據(jù)的安全性,但它也會(huì)帶來(lái)一定的性能開(kāi)銷(xiāo)。尤其是在處理大量數(shù)據(jù)時(shí),這可能會(huì)成為系統(tǒng)的瓶頸。下面,我們就來(lái)看看如何優(yōu)化加密解密的性能。
1. 選擇合適的加密算法
不同的加密算法在性能上有很大差異。比如,AES 算法的速度比 RSA 算法快得多。如果你的項(xiàng)目對(duì)性能要求較高,建議選擇 AES 這樣的對(duì)稱(chēng)加密算法。如果需要非對(duì)稱(chēng)加密,可以考慮使用 RSA 來(lái)加密 AES 的密鑰,然后用 AES 來(lái)加密實(shí)際數(shù)據(jù),這樣可以在安全性和性能之間找到一個(gè)平衡點(diǎn)。
2. 緩存加密后的結(jié)果
如果某些數(shù)據(jù)在短時(shí)間內(nèi)會(huì)被頻繁訪(fǎng)問(wèn),你可以考慮將加密后的結(jié)果緩存起來(lái)。比如,使用 Redis 這樣的分布式緩存,將加密后的數(shù)據(jù)存儲(chǔ)在緩存中。這樣,當(dāng)再次訪(fǎng)問(wèn)這些數(shù)據(jù)時(shí),就可以直接從緩存中獲取,而不需要進(jìn)行解密操作。
3. 異步處理加密解密
如果加密解密的操作比較耗時(shí),你可以考慮將其放到異步線(xiàn)程中處理。比如,在插入數(shù)據(jù)時(shí),先將明文數(shù)據(jù)存入數(shù)據(jù)庫(kù),然后在異步線(xiàn)程中進(jìn)行加密,并更新數(shù)據(jù)庫(kù)中的數(shù)據(jù)。這樣,主業(yè)務(wù)線(xiàn)程就不會(huì)被加密操作阻塞,提高了系統(tǒng)的響應(yīng)速度。
4. 批量處理
如果需要處理大量的數(shù)據(jù),批量處理可以顯著提高性能。比如,在插入數(shù)據(jù)時(shí),將多個(gè)數(shù)據(jù)對(duì)象一次性加密,然后批量插入數(shù)據(jù)庫(kù)。這樣可以減少數(shù)據(jù)庫(kù)的交互次數(shù),提高吞吐量。
六、安全注意事項(xiàng):別讓你的加密 “裸奔”
雖然我們已經(jīng)實(shí)現(xiàn)了加密解密功能,但如果不注意安全細(xì)節(jié),你的系統(tǒng)可能還是漏洞百出。下面,我們就來(lái)看看需要注意的安全事項(xiàng)。
1. 密鑰管理
密鑰是加密解密的核心,一旦泄露,所有的加密數(shù)據(jù)都將形同虛設(shè)。因此,密鑰的管理至關(guān)重要:
- 不要將密鑰硬編碼在代碼中,應(yīng)該通過(guò)配置文件、環(huán)境變量或配置中心來(lái)獲取。
- 定期更換密鑰,防止長(zhǎng)期使用同一密鑰導(dǎo)致的安全風(fēng)險(xiǎn)。
- 對(duì)密鑰進(jìn)行嚴(yán)格的訪(fǎng)問(wèn)控制,只有授權(quán)的人員才能訪(fǎng)問(wèn)密鑰。
2. 加密算法的選擇
不要使用已經(jīng)被證明不安全的加密算法,比如 MD5、SHA-1 等。建議選擇 AES、SM4 等安全的加密算法。如果你的項(xiàng)目涉及國(guó)家秘密,還需要使用國(guó)密算法,如 SM2、SM3、SM4。
3. 數(shù)據(jù)傳輸安全
加密解密不僅要考慮數(shù)據(jù)的存儲(chǔ)安全,還要考慮數(shù)據(jù)在傳輸過(guò)程中的安全。建議使用 HTTPS 來(lái)加密數(shù)據(jù)傳輸,防止中間人攻擊。
4. 日志安全
在日志中不要輸出加密后的明文數(shù)據(jù)。比如,在記錄用戶(hù)信息時(shí),應(yīng)該只記錄加密后的數(shù)據(jù),或者對(duì)敏感字段進(jìn)行脫敏處理。否則,日志文件可能會(huì)成為黑客獲取敏感信息的突破口。
七、總結(jié):擁抱自動(dòng)化,告別手動(dòng)搬磚
通過(guò) MyBatis 的自定義類(lèi)型處理器、攔截器和 MyBatis-Plus 的內(nèi)置功能,我們可以輕松實(shí)現(xiàn)數(shù)據(jù)的加密解密,徹底扔掉那個(gè)笨重的工具類(lèi)。這種自動(dòng)化的處理方式不僅減少了代碼冗余,提高了開(kāi)發(fā)效率,還增強(qiáng)了系統(tǒng)的安全性和可維護(hù)性。
當(dāng)然,加密解密只是數(shù)據(jù)安全的一部分。在實(shí)際項(xiàng)目中,還需要結(jié)合訪(fǎng)問(wèn)控制、數(shù)據(jù)脫敏、安全審計(jì)等多種手段,構(gòu)建全方位的數(shù)據(jù)安全防護(hù)體系。






























