基于MyBatis插件實(shí)現(xiàn)字段加解密的實(shí)現(xiàn)示例
前言
對于大多數(shù)系統(tǒng)來說,敏感數(shù)據(jù)的加密存儲都是必須考慮和實(shí)現(xiàn)的。最近在公司的項(xiàng)目中也接到了相關(guān)的安全需求,因?yàn)轫?xiàng)目使用了 MyBatis 作為數(shù)據(jù)庫持久層框架,在經(jīng)過一番調(diào)研后決定使用其插件機(jī)制來實(shí)現(xiàn)字段加解密功能,并且封裝成一個輕量級、支持配置、方便擴(kuò)展的組件提供給其他項(xiàng)目使用。
MyBatis 的插件機(jī)制
簡介
MyBatis 提供了插件功能,它允許你攔截 MyBatis 執(zhí)行過程中的某個方法,對其增加自定義操作。默認(rèn)情況下,MyBatis 允許攔截的方法包括:
| 類 | 方法 | 說明 |
|---|---|---|
| org.apache.ibatis.executor.Executor | update, query, flushStatements, commit, rollback, getTransaction, close, isClosed | 攔截執(zhí)行器的方法 |
| org.apache.ibatis.executor.parameter.ParameterHandler | getParameterObject, setParameters | 說明:攔截參數(shù)處理的方法 |
| org.apache.ibatis.executor.resultset.ResultSetHandler | handleResultSets, handleOutputParameters | 攔截結(jié)果集處理的方法 |
| org.apache.ibatis.executor.statement.StatementHandler | prepare, parameterize, batch, update, query | 攔截 Sql 語句構(gòu)建的方法 |
插件實(shí)現(xiàn)
在 MyBatis 中,一個插件其實(shí)就是一個攔截器,插件的實(shí)現(xiàn)方式非常簡單,只需要實(shí)現(xiàn) org.apache.ibatis.plugin.Interceptor 接口,并且通過 @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;
}
}
上面的插件會攔截 org.apache.ibatis.executor.Executor#update 方法的所有調(diào)用,你可以在 invocation.proceed() 前后增加插件邏輯。
字段加解密實(shí)現(xiàn)
對于字段加解密來說,需要關(guān)注的點(diǎn)就是查詢、插入和更新,在進(jìn)行這些操作的時候需要對字段進(jìn)行處理(插入、更新時加密,查詢時解密),與上文提到的攔截點(diǎn)的對應(yīng)關(guān)系如下:
- 插入、更新:
org.apache.ibatis.executor.Executor#update - 查詢:
org.apache.ibatis.executor.resultset.ResultSetHandler#handleResultSets
代碼實(shí)現(xiàn)
先上完整代碼:github.com/WhiteDG/myb…
一般場景下只有包含敏感數(shù)據(jù)的字段才需要進(jìn)行加解密,所以需要一個注解來標(biāo)記哪些字段需要加解密,這里定義為 @EncryptedField,提供兩個屬性:
- key:加解密時用到的密鑰
- encryptor:指定加解密器,不指定則使用全局的加解密器
@Documented
@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptedField {
String key() default "";
Class<? extends IEncryptor> encryptor() default IEncryptor.class;
}
加密的整體思路就是通過 Invocation 拿到方法參數(shù),有兩種情況:一種是實(shí)體類,一種是 ParamMap,實(shí)體類通過注解確定需要加密的字段,ParamMap 通過配置的參數(shù)名前綴確定需要加密的字段,然后使用加密器對需要加密的字段進(jìn)行加密覆蓋掉原始值即可。如果是實(shí)體類則在方法執(zhí)行完成后還需要對 key 字段進(jìn)行回寫處理。 在對參數(shù)進(jìn)行處理前使用 Kryo 拷貝了一份源數(shù)據(jù),目的是保留方法調(diào)用時的原始參數(shù),避免經(jīng)過插件的 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();
}
}
解密插件與加密插件類似,通過 Invocation 拿到查詢 SQL 執(zhí)行后返回的結(jié)果集,有兩種情況:一種是返回 ArrayList,一種是返回單個實(shí)體,同樣通過注解確定需要解密的字段對其進(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)
作為一個通用的組件(這里命名為 mybatis-crypto),支持配置和方便擴(kuò)展是基本的要求。
mybatis-crypto 提供了以下幾個配置項(xiàng)滿足基本使用:
| 配置項(xiàng) | 說明 | 默認(rèn)值 |
|---|---|---|
| mybatis-crypto.enabled | 是否啟用 mybatis-crypto | true |
| mybatis-crypto.fail-fast | 快速失敗,加解密過程中發(fā)生異常是否中斷。true:拋出異常,false:使用原始值,打印 warn 級別日志 | true |
| mybatis-crypto.mapped-key-prefixes | @Param 參數(shù)名的前綴,前綴匹配則會進(jìn)行加密處理 | 空 |
| mybatis-crypto.default-encryptor | 全局默認(rèn) Encryptor | 空 |
| mybatis-crypto.default-key | 全局默認(rèn) Encryptor 的密鑰 | 空 |
mybatis-crypto 核心包默認(rèn)不提供具體的加解密方法,開發(fā)者可以通過引入 mybatis-crypto-encryptors 使用其提供的常用加解密類,或者實(shí)現(xiàn) io.github.whitedg.mybatis.crypto.IEncryptor 自行擴(kuò)展加解密方法。
Starter 封裝
目前大多數(shù)項(xiàng)目都是基于 spring-boot 進(jìn)行開發(fā)的,所以將 mybatis-crypto 封裝成一個 starter 會更方便開發(fā)者使用。starter 的主要工作就是讀取配置,自動裝配,因此它的實(shí)現(xiàn)非常簡單,只有兩個類,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é)
本文簡單介紹了基于 mybatis 插件機(jī)制實(shí)現(xiàn)字段加解密的思路及流程,并將其封裝成一個通用的 spring-boot-starter 組件,開發(fā)者可以方便的引入使用,同時也提供了加解密方法集合 mybatis-crypto-encryptors。
組件的具體使用方法和示https://github.com/WhiteDG/mybatis-crypto
到此這篇關(guān)于基于MyBatis插件實(shí)現(xiàn)字段加解密的實(shí)現(xiàn)示例的文章就介紹到這了,更多相關(guān)MyBatis 字段加解密內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Maven分模塊開發(fā)與依賴管理和聚合和繼承及屬性深入詳細(xì)介紹
依賴管理是項(xiàng)目管理中非常重要的一環(huán)。幾乎任何項(xiàng)目開發(fā)的時候需要都需要使用到庫。而這些庫很可能又依賴別的庫,這樣整個項(xiàng)目的依賴形成了一個樹狀結(jié)構(gòu),而隨著這個依賴的樹的延伸和擴(kuò)大,一系列問題就會隨之產(chǎn)生2022-10-10
Java BigDecimal類的使用和注意事項(xiàng)
這篇文章主要講解Java中BigDecimal類的用法,并簡單介紹一些注意事項(xiàng),希望能給大家做一個參考。2016-06-06
簡單談?wù)凾hreadPoolExecutor線程池之submit方法
下面小編就為大家?guī)硪黄唵握務(wù)凾hreadPoolExecutor線程池之submit方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-06-06
flowable動態(tài)創(chuàng)建多級流程模板實(shí)現(xiàn)demo
這篇文章主要為大家介紹了flowable動態(tài)創(chuàng)建多級流程模板實(shí)現(xiàn)demo,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05
SpringBoot使用Jasypt對YML文件配置內(nèi)容加密的方法(數(shù)據(jù)庫密碼加密)
本文介紹了如何在SpringBoot項(xiàng)目中使用Jasypt對application.yml文件中的敏感信息(如數(shù)據(jù)庫密碼)進(jìn)行加密,通過引入Jasypt依賴、配置加密密鑰、加密敏感信息并測試解密功能,可以提高配置文件的安全性,減少因配置文件泄露導(dǎo)致的安全風(fēng)險,感興趣的朋友一起看看吧2025-03-03
Spring Boot 如何使用Liquibase 進(jìn)行數(shù)據(jù)庫遷移(操作方法)
在Spring Boot應(yīng)用程序中使用Liquibase進(jìn)行數(shù)據(jù)庫遷移是一種強(qiáng)大的方式來管理數(shù)據(jù)庫模式的變化,本文重點(diǎn)講解如何在Spring Boot應(yīng)用程序中使用Liquibase進(jìn)行數(shù)據(jù)庫遷移,從而更好地管理數(shù)據(jù)庫模式的變化,感興趣的朋友跟隨小編一起看看吧2023-09-09

