基于MyBatis插件實(shí)現(xiàn)字段加解密的實(shí)現(xiàn)示例
前言
對(duì)于大多數(shù)系統(tǒng)來(lái)說(shuō),敏感數(shù)據(jù)的加密存儲(chǔ)都是必須考慮和實(shí)現(xiàn)的。最近在公司的項(xiàng)目中也接到了相關(guān)的安全需求,因?yàn)轫?xiàng)目使用了 MyBatis 作為數(shù)據(jù)庫(kù)持久層框架,在經(jīng)過(guò)一番調(diào)研后決定使用其插件機(jī)制來(lái)實(shí)現(xiàn)字段加解密功能,并且封裝成一個(gè)輕量級(jí)、支持配置、方便擴(kuò)展的組件提供給其他項(xiàng)目使用。
MyBatis 的插件機(jī)制
簡(jiǎn)介
MyBatis 提供了插件功能,它允許你攔截 MyBatis 執(zhí)行過(guò)程中的某個(gè)方法,對(duì)其增加自定義操作。默認(rèn)情況下,MyBatis 允許攔截的方法包括:
| 類(lèi) | 方法 | 說(shuō)明 |
|---|---|---|
| org.apache.ibatis.executor.Executor | update, query, flushStatements, commit, rollback, getTransaction, close, isClosed | 攔截執(zhí)行器的方法 |
| org.apache.ibatis.executor.parameter.ParameterHandler | getParameterObject, setParameters | 說(shuō)明:攔截參數(shù)處理的方法 |
| org.apache.ibatis.executor.resultset.ResultSetHandler | handleResultSets, handleOutputParameters | 攔截結(jié)果集處理的方法 |
| org.apache.ibatis.executor.statement.StatementHandler | prepare, parameterize, batch, update, query | 攔截 Sql 語(yǔ)句構(gòu)建的方法 |
插件實(shí)現(xiàn)
在 MyBatis 中,一個(gè)插件其實(shí)就是一個(gè)攔截器,插件的實(shí)現(xiàn)方式非常簡(jiǎn)單,只需要實(shí)現(xiàn) org.apache.ibatis.plugin.Interceptor 接口,并且通過(guò) @Intercepts 注解指定要攔截的方法簽名即可。 以下是官方文檔提供的例子:
// ExamplePlugin.java
@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
private Properties properties = new Properties();
@Override
public Object intercept(Invocation invocation) throws Throwable {
// implement pre-processing if needed
Object returnObject = invocation.proceed();
// implement post-processing if needed
return returnObject;
}
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
}
上面的插件會(huì)攔截 org.apache.ibatis.executor.Executor#update 方法的所有調(diào)用,你可以在 invocation.proceed() 前后增加插件邏輯。
字段加解密實(shí)現(xiàn)
對(duì)于字段加解密來(lái)說(shuō),需要關(guān)注的點(diǎn)就是查詢、插入和更新,在進(jìn)行這些操作的時(shí)候需要對(duì)字段進(jìn)行處理(插入、更新時(shí)加密,查詢時(shí)解密),與上文提到的攔截點(diǎn)的對(duì)應(yīng)關(guān)系如下:
- 插入、更新:
org.apache.ibatis.executor.Executor#update - 查詢:
org.apache.ibatis.executor.resultset.ResultSetHandler#handleResultSets
代碼實(shí)現(xiàn)
先上完整代碼:github.com/WhiteDG/myb…
一般場(chǎng)景下只有包含敏感數(shù)據(jù)的字段才需要進(jìn)行加解密,所以需要一個(gè)注解來(lái)標(biāo)記哪些字段需要加解密,這里定義為 @EncryptedField,提供兩個(gè)屬性:
- key:加解密時(shí)用到的密鑰
- encryptor:指定加解密器,不指定則使用全局的加解密器
@Documented
@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptedField {
String key() default "";
Class<? extends IEncryptor> encryptor() default IEncryptor.class;
}
加密的整體思路就是通過(guò) Invocation 拿到方法參數(shù),有兩種情況:一種是實(shí)體類(lèi),一種是 ParamMap,實(shí)體類(lèi)通過(guò)注解確定需要加密的字段,ParamMap 通過(guò)配置的參數(shù)名前綴確定需要加密的字段,然后使用加密器對(duì)需要加密的字段進(jìn)行加密覆蓋掉原始值即可。如果是實(shí)體類(lèi)則在方法執(zhí)行完成后還需要對(duì) key 字段進(jìn)行回寫(xiě)處理。 在對(duì)參數(shù)進(jìn)行處理前使用 Kryo 拷貝了一份源數(shù)據(jù),目的是保留方法調(diào)用時(shí)的原始參數(shù),避免經(jīng)過(guò)插件的 SQL 執(zhí)行完成后,原始參數(shù)變成已加密的。
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
if (Util.encryptionRequired(parameter, ms.getSqlCommandType())) {
Kryo kryo = null;
try {
kryo = KryoPool.obtain();
Object copiedParameter = kryo.copy(parameter);
boolean isParamMap = parameter instanceof MapperMethod.ParamMap;
if (isParamMap) {
//noinspection unchecked
MapperMethod.ParamMap<Object> paramMap = (MapperMethod.ParamMap<Object>) copiedParameter;
encryptParamMap(paramMap);
} else {
encryptEntity(copiedParameter);
}
args[1] = copiedParameter;
Object result = invocation.proceed();
if (!isParamMap) {
handleKeyProperties(ms, parameter, copiedParameter);
}
return result;
} finally {
if (kryo != null) {
KryoPool.free(kryo);
}
}
} else {
return invocation.proceed();
}
}
解密插件與加密插件類(lèi)似,通過(guò) Invocation 拿到查詢 SQL 執(zhí)行后返回的結(jié)果集,有兩種情況:一種是返回 ArrayList,一種是返回單個(gè)實(shí)體,同樣通過(guò)注解確定需要解密的字段對(duì)其進(jìn)行解密然后覆蓋掉原始加密的值。
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
if (result == null) {
return null;
}
if (result instanceof ArrayList) {
//noinspection rawtypes
ArrayList resultList = (ArrayList) result;
if (resultList.isEmpty()) {
return result;
}
Object firstItem = resultList.get(0);
boolean needToDecrypt = Util.decryptionRequired(firstItem);
if (!needToDecrypt) {
return result;
}
Set<Field> encryptedFields = EncryptedFieldsProvider.get(firstItem.getClass());
if (encryptedFields == null || encryptedFields.isEmpty()) {
return result;
}
for (Object item : resultList) {
decryptEntity(encryptedFields, item);
}
} else {
if (Util.decryptionRequired(result)) {
decryptEntity(EncryptedFieldsProvider.get(result.getClass()), result);
}
}
return result;
}
支持配置、方便擴(kuò)展的實(shí)現(xiàn)
作為一個(gè)通用的組件(這里命名為 mybatis-crypto),支持配置和方便擴(kuò)展是基本的要求。
mybatis-crypto 提供了以下幾個(gè)配置項(xiàng)滿足基本使用:
| 配置項(xiàng) | 說(shuō)明 | 默認(rèn)值 |
|---|---|---|
| mybatis-crypto.enabled | 是否啟用 mybatis-crypto | true |
| mybatis-crypto.fail-fast | 快速失敗,加解密過(guò)程中發(fā)生異常是否中斷。true:拋出異常,false:使用原始值,打印 warn 級(jí)別日志 | true |
| mybatis-crypto.mapped-key-prefixes | @Param 參數(shù)名的前綴,前綴匹配則會(huì)進(jìn)行加密處理 | 空 |
| mybatis-crypto.default-encryptor | 全局默認(rèn) Encryptor | 空 |
| mybatis-crypto.default-key | 全局默認(rèn) Encryptor 的密鑰 | 空 |
mybatis-crypto 核心包默認(rèn)不提供具體的加解密方法,開(kāi)發(fā)者可以通過(guò)引入 mybatis-crypto-encryptors 使用其提供的常用加解密類(lèi),或者實(shí)現(xiàn) io.github.whitedg.mybatis.crypto.IEncryptor 自行擴(kuò)展加解密方法。
Starter 封裝
目前大多數(shù)項(xiàng)目都是基于 spring-boot 進(jìn)行開(kāi)發(fā)的,所以將 mybatis-crypto 封裝成一個(gè) starter 會(huì)更方便開(kāi)發(fā)者使用。starter 的主要工作就是讀取配置,自動(dòng)裝配,因此它的實(shí)現(xiàn)非常簡(jiǎn)單,只有兩個(gè)類(lèi),MybatisCryptoProperties 獲取配置,MyBatisCryptoAutoConfiguration 加載插件。
@Configuration
@ConditionalOnProperty(value = "mybatis-crypto.enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(MybatisCryptoProperties.class)
public class MyBatisCryptoAutoConfiguration {
@Bean
@ConditionalOnMissingBean(MybatisEncryptionPlugin.class)
public MybatisEncryptionPlugin encryptionInterceptor(MybatisCryptoProperties properties) {
return new MybatisEncryptionPlugin(properties.toMybatisCryptoConfig());
}
@Bean
@ConditionalOnMissingBean(MybatisDecryptionPlugin.class)
public MybatisDecryptionPlugin decryptionInterceptor(MybatisCryptoProperties properties) {
return new MybatisDecryptionPlugin(properties.toMybatisCryptoConfig());
}
}
總結(jié)
本文簡(jiǎn)單介紹了基于 mybatis 插件機(jī)制實(shí)現(xiàn)字段加解密的思路及流程,并將其封裝成一個(gè)通用的 spring-boot-starter 組件,開(kāi)發(fā)者可以方便的引入使用,同時(shí)也提供了加解密方法集合 mybatis-crypto-encryptors。
組件的具體使用方法和示https://github.com/WhiteDG/mybatis-crypto
到此這篇關(guān)于基于MyBatis插件實(shí)現(xiàn)字段加解密的實(shí)現(xiàn)示例的文章就介紹到這了,更多相關(guān)MyBatis 字段加解密內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中ConstraintValidator接口使用方法詳解
在現(xiàn)代 Java 應(yīng)用開(kāi)發(fā)中,數(shù)據(jù)校驗(yàn)是保證系統(tǒng)健壯性和數(shù)據(jù)一致性的核心環(huán)節(jié),無(wú)論是 Web 請(qǐng)求參數(shù)、數(shù)據(jù)庫(kù)實(shí)體對(duì)象,還是業(yè)務(wù)邏輯中的中間狀態(tài),都需要通過(guò)嚴(yán)格的校驗(yàn)規(guī)則來(lái)確保數(shù)據(jù)的合法性,Java 提供了 Bean Validation,本文給大家介紹了ConstraintValidator接口使用方法2025-07-07
Mybatis自定義TypeHandler解決特殊類(lèi)型轉(zhuǎn)換問(wèn)題詳解
這篇文章主要介紹了Mybatis自定義TypeHandler解決特殊類(lèi)型轉(zhuǎn)換問(wèn)題詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11
j2ee之AJAX二級(jí)聯(lián)動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了j2ee之AJAX二級(jí)聯(lián)動(dòng)效果的實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08
Java實(shí)現(xiàn)矩陣加減乘除及轉(zhuǎn)制等運(yùn)算功能示例
這篇文章主要介紹了Java實(shí)現(xiàn)矩陣加減乘除及轉(zhuǎn)制等運(yùn)算功能,結(jié)合實(shí)例形式總結(jié)分析了java常見(jiàn)的矩陣運(yùn)算實(shí)現(xiàn)技巧,需要的朋友可以參考下2018-01-01
關(guān)于ArrayList初始化容量的問(wèn)題
這篇文章主要介紹了關(guān)于ArrayList初始化容量的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03
Maven Profile多環(huán)境構(gòu)建實(shí)戰(zhàn)指南
在現(xiàn)代企業(yè)級(jí)Java項(xiàng)目開(kāi)發(fā)中,面對(duì)開(kāi)發(fā)(dev)、測(cè)試(test)、預(yù)發(fā)布(staging)、生產(chǎn)(prod)等多套環(huán)境的差異化配置管理,本文將從實(shí)戰(zhàn)角度深入剖析Maven Profile的多環(huán)境構(gòu)建體系,需要的朋友可以參考下2025-05-05
Java后臺(tái)定時(shí)器導(dǎo)致系統(tǒng)崩潰的原因和解決方法
這篇文章主要介紹了Java后臺(tái)定時(shí)器導(dǎo)致系統(tǒng)崩潰的原因和解決方法,主要涵蓋了常見(jiàn)定時(shí)器類(lèi)型、系統(tǒng)崩潰的常見(jiàn)根因、真場(chǎng)景總結(jié)以及具體案例,需要的朋友可以參考下2025-11-11
java實(shí)現(xiàn)文件分片上傳并且斷點(diǎn)續(xù)傳的示例代碼
本文主要介紹了java實(shí)現(xiàn)文件分片上傳并且斷點(diǎn)續(xù)傳的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05

