文章目錄
- Mybatis數據加密解密
- 一、自定義注解
- 二、自定義參數處理攔截器
- 結果集攔截器
- 加密解密
Mybatis數據加密解密
方案一:Mybatis攔截器之數據加密解密【Interceptor】
攔截器介紹
Mybatis Interceptor 在 Mybatis 中被當作 Plugin(插件),不知道為什么,但確實是在 org.apache.ibatis.plugin 包下面
既然是攔截器,可以攔截哪些內容呢?試想一下… 當程序寫到持久層時,Mybatis 會 執行 指定 SQL 語句,并處理 請求參數 和 返回值。沒錯,Mybatis 攔截器可以幫助我們處理上述內容,請看官網的 Plugins 的片段, 內容不多
// 執行
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
// 請求參數處理
ParameterHandler (getParameterObject, setParameters)
// 返回結果集處理
ResultSetHandler (handleResultSets, handleOutputParameters)
// SQL語句構建
StatementHandler (prepare, parameterize, batch, update, query)
攔截器的使用
如果需要實現自定義的攔截器,只需要實現 org.apache.ibatis.plugin.Interceptor 接口,該接口有三個方法:
Object intercept(Invocation invocation) throws Throwable;Object plugin(Object target);void setProperties(Properties properties);
我們要實現數據加密,進入數據庫的字段不能是真實的數據,但是返回來的數據要真實可用,所以我們需要針對 Parameter 和 ResultSet 兩種類型處理,同時為了更靈活的使用,我們需要自定義注解
一、自定義注解
類注解,將注解放在實體類上
/*** 需要加解密的類注解*/
@Documented
@Inherited
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptDecryptClass {
}
字段注解,將注解放在實體字段上
/*** 加密字段注解*/
@Documented
@Inherited
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptDecryptField {}
有了這兩個注解,我們可以在我們可以標記我們要處理的實體和實體中的字段
二、自定義參數處理攔截器
參考官網,通過 @Intercepts 和 @Signature 的聯合使用,指定 ParameterHandler.class 類型,同時通過 @Component 注解注入到容器中,即可在設置參數的時候進行攔截,通過自定義接口 IEncryptDecrypt, 根據 Field 的各種類型自定義加密解密算法
@Intercepts({@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
@ConditionalOnProperty(value = "domain.encrypt", havingValue = "true")
@Component
@Slf4j
public class ParammeterInterceptor implements Interceptor {@Autowiredprivate IEncryptDecrypt encryptDecrypt;@Overridepublic Object intercept(Invocation invocation) throws Throwable {log.info("攔截器ParamInterceptor");//攔截 ParameterHandler 的 setParameters 方法 動態設置參數if (invocation.getTarget() instanceof ParameterHandler) {ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0];// 反射獲取 BoundSql 對象,此對象包含生成的sql和sql的參數map映射/*Field boundSqlField = parameterHandler.getClass().getDeclaredField("boundSql");boundSqlField.setAccessible(true);BoundSql boundSql = (BoundSql) boundSqlField.get(parameterHandler);*/// 反射獲取 參數對像Field parameterField =parameterHandler.getClass().getDeclaredField("parameterObject");parameterField.setAccessible(true);Object parameterObject = parameterField.get(parameterHandler);if (Objects.nonNull(parameterObject)){Class<?> parameterObjectClass = parameterObject.getClass();EncryptDecryptClass encryptDecryptClass = AnnotationUtils.findAnnotation(parameterObjectClass, EncryptDecryptClass.class);if (Objects.nonNull(encryptDecryptClass)){Field[] declaredFields = parameterObjectClass.getDeclaredFields();final Object encrypt = encryptDecrypt.encrypt(declaredFields, parameterObject);}}}return invocation.proceed();}@Overridepublic Object plugin(Object o) {return Plugin.wrap(o, this);}@Overridepublic void setProperties(Properties properties) {}
}
同樣新建結果集攔截器
結果集攔截器
與參數攔截器基本一樣, 只不過類型指定為 ResultSetHandler.class
@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args={Statement.class})
})
@ConditionalOnProperty(value = "domain.decrypt", havingValue = "true")
@Component
@Slf4j
public class ResultInterceptor implements Interceptor {@Autowiredprivate IEncryptDecrypt encryptDecrypt;@Overridepublic Object intercept(Invocation invocation) throws Throwable {Object result = invocation.proceed();if (Objects.isNull(result)){return null;}if (result instanceof ArrayList) {ArrayList resultList = (ArrayList) result;if (CollectionUtils.isNotEmpty(resultList) && needToDecrypt(resultList.get(0))){for (int i = 0; i < resultList.size(); i++) {encryptDecrypt.decrypt(resultList.get(i));}}}else {if (needToDecrypt(result)){encryptDecrypt.decrypt(result);}}return result;}public boolean needToDecrypt(Object object){Class<?> objectClass = object.getClass();EncryptDecryptClass encryptDecryptClass = AnnotationUtils.findAnnotation(objectClass, EncryptDecryptClass.class);if (Objects.nonNull(encryptDecryptClass)){return true;}return false;}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {}
}
加密解密
IEncryptDecrypt 接口定義了 加密和解密兩個方法,
public interface IEncryptDecrypt {/*** 加密方法* @param declaredFields 反射bean成員變量* @param parameterObject Mybatis入參* @param <T>* @return*/public <T> T encrypt(Field[] declaredFields, T parameterObject) throws IllegalAccessException;/*** 解密方法* @param result Mybatis 返回值,需要判斷是否是ArrayList類型* @param <T>* @return*/public <T> T decrypt(T result) throws IllegalAccessException;
}
兩個攔截器通過在 YAML 中配置屬性,按條件注入,外加自定義加密解密算法,完成全局靈活的配置。
核心代碼已上傳至 Github Demo
方案二:Mybatis利用內置類型轉換器【typeHandler】
mybatis 利用內置類型轉換器(「typeHandler」),實現 Java 類型與 JDBC 類型的相互轉換,我們正好可以利用這個特性,在轉換之前加入加解密步驟。
typeHandler 底層原理不是復雜,如果我們沒有使用 Mybatis,而是直接使用最原始的 JDBC 執行查詢語句,相關代碼如下:
我們需要手動判斷 Java 類型,然后調用 PreparedStatement設置合適類型參數。獲取返回結果之后,又需要手動調用 ResultSet 結果集獲取相應類型的數據,這個過程十分繁瑣。使用 mybatis 之后,上述步驟就無需我們再實現了。mybatis 可以通過識別 Java/JDBC 類型,調用相應typeHandler,自動實現轉換邏輯。下圖為 mybatis 內置類型轉換器,基本涵蓋了所有 「Java/JDBC」 數據類型。
通用解決方案
自定義 typeHandler
下面我們來實現帶有加解密功能的類型轉換器,實現方式也比較簡單,只要繼承 org.apache.ibatis.type.BaseTypeHandler,重寫相關方法。
簡單起見,上述加解密僅使用了 Base64,大家可以替換成相應加解密算法即或者引入相應加解密服務。
在這里插入圖片描述
其中加密轉換將在 setNonNullParameter 中執行,解密轉換將在 getNullableResult中執行。CryptTypeHandler 使用一個 MappedTypes 注解,包含一個 CryptType 類,這個類使用 mybatis 別名功能,可以極大簡化 sqlmap 相關配置。
注冊 typeHandler
使用方必須將 typeHandler 和 alias 注冊到 mybatis 中,否則無法生效。下面提供三種方式,可以根據項目情況選擇其中一種即可:
「單獨使用 mybatis」
這種場景需要在 「mybatis-config.xml」 配置,mybatis 啟動時將會加載該配置文件。
<typeHandlers><!--類型轉換器包路徑--><package name="com.xx.xx"/>
</typeHandlers><!-- 別名定義 -->
<typeAliases><!-- 針對單個別名定義 type:類型的路徑 alias:別名 --><typeAlias type="xx.xx.xx" alias="xx"/>
</typeAliases>
「使用 Spring 配置 Mybatis Bean」
配合 Spring 使用時需要將 typeHandler 注入 SqlSessionFactoryBean ,配置方式如下:
<!-- MyBatis 工廠 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"><property name="dataSource" ref="dataSource" /><!--alias 注入--><property name="typeAliasesPackage" value="xx.xx.xx"/><!-- typeHandlers 注入 --><property name="typeHandlersPackage" value="xx.xx.xx"/>
</bean>
「SpringBoot」
SpringBoot 方式就最簡單了,只要引入 mybatis-starter,配置文件加入如下配置即可:
## mybatis 配置
# 類型轉換器包路徑
mybatis.type-handlers-package=com.xx.xx.x
mybatis.type-aliases-package=com.xx.xx
修改 mapper sql 配置
最后我們只要簡單修改 mapper 中 resultMap 或 sql s配置就可以實現加解密。假設我們對現有一張 「bank_card」 表進行加解密,表結構如下:
CREATE TABLE bank_card (
id int primary key auto_increment,
gmt_create timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
gmt_update timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
card_no varchar(256) NOT NULL DEFAULT '' COMMENT '卡號',
phone varchar(256) NOT NULL DEFAULT '' COMMENT '手機號',
name varchar(256) NOT NULL DEFAULT '' COMMENT '姓名',
id_no varchar(256) NOT NULL DEFAULT '' COMMENT '證件號'
);
「insert 加密」
現需要對 card_no,phone,name,id_no 進行加密,「insert」 語句加密示例:
<insert id="insertBankCard" keyProperty="id" useGeneratedKeys="true" parameterType="org.demo.pojo.BankCardDO">INSERT INTO bank_card (card_no, phone,name,id_no)VALUES(#{card_no,javaType=crypt},#{phone,typeHandler=org.demo.type.CryptTypeHandler},#{name,javaType=crypt},#{id_no,javaType=crypt})
</insert>
我們只需要在 「#{}」 指定 typeHandler,傳入參數最后將被加密。使用 typeHandler需要使用類的全路徑,比較繁瑣,我們可以使用 「javaType」 屬性,直接使用上面我們的定義別名 「crypt」。數據庫最終執行sql 如下:
INSERT INTO bank_card (card_no, phone,name,id_no) VALUES ('NjQzMjEyMzEyMzE=', 'MTM1Njc4OTEyMzQ=', '5rWL6K+V5Y2h', 'MTIzMTIzMTIzMQ==');
推薦一款 IDEA 的插件 「mybatis-log-plugin」,可以自動將 mybatis sql 日志還原成真實執行 sql
「查詢加解密」普通查詢解密示例如下:
<resultMap id="bankCardXml" type="org.demo.pojo.BankCardDO"><result property="card_no" column="card_no" typeHandler="org.demo.type.CryptTypeHandler"/><result property="name" column="name" typeHandler="org.demo.type.CryptTypeHandler"/><result property="id_no" column="id_no" typeHandler="org.demo.type.CryptTypeHandler"/><result property="phone" column="phone" typeHandler="org.demo.type.CryptTypeHandler"/>
</resultMap>
<select id="queryById" resultMap="bankCardXml">select * from bank_card where id=#{id}
</select>
這里我們在 「select」 配置中只能使用 resultMap 屬性,指定 typeHandler 。數據庫明文、密文共存的情況,查詢解密示例如下:
<!-- resultMap 同上 -->
<select id="queryByPhone" resultMap="bankCardXml">select * from bank_card where phone in(#{card_no,javaType=crypt},#{card_no})
</select>
最后我們可以將自定義的 typeHandler 單獨打包發布,其他業務方只需要引用,改造相關配置文件,即可完成數據加解密。上述代碼示例已上傳至 Github
總結
借助于自定義的 typeHandler,我們實現了一個通用的加解密的方案,該方案對于使用方來說代碼侵入性小,開箱即用,可以快速完成加解密的改造。