Mybatis-Plus根據(jù)自定義注解實(shí)現(xiàn)自動(dòng)加解密的示例代碼
背景
我們把數(shù)據(jù)存到數(shù)據(jù)庫(kù)的時(shí)候,有些敏感字段是需要加密的,從數(shù)據(jù)庫(kù)查出來(lái)再進(jìn)行解密。如果存在多張表或者多個(gè)地方需要對(duì)部分字段進(jìn)行加解密操作,每個(gè)地方都手寫一次加解密的動(dòng)作,顯然不是最好的選擇。如果我們使用的是Mybatis框架,那就跟著一起探索下如何使用框架的攔截器功能實(shí)現(xiàn)自動(dòng)加解密吧。
定義一個(gè)自定義注解
我們需要一個(gè)注解,只要實(shí)體類的屬性加上這個(gè)注解,那么就對(duì)這個(gè)屬性進(jìn)行自動(dòng)加解密。我們把這個(gè)注解定義靈活一點(diǎn),不僅可以放在屬性上,還可以放到類上,如果在類上使用這個(gè)注解,代表這個(gè)類的所有屬性都進(jìn)行自動(dòng)加密。
/** * 加密字段 */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.TYPE}) public @interface EncryptField { }
定義實(shí)體類
package com.wen3.demo.mybatisplus.po; import com.baomidou.mybatisplus.annotation.*; import com.wen3.demo.mybatisplus.encrypt.annotation.EncryptField; import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; @EncryptField @Getter @Setter @Accessors(chain = true) @KeySequence(value = "t_user_user_id_seq", dbType = DbType.POSTGRE_SQL) @TableName("t_USER") public class UserPo { /** * 用戶id */ @TableId(value = "USER_ID", type = IdType.INPUT) private Long userId; /** * 用戶姓名 */ @TableField("USER_NAME") private String userName; /** * 用戶性別 */ @TableField("USER_SEX") private String userSex; /** * 用戶郵箱 */ @EncryptField @TableField("USER_EMAIL") private String userEmail; /** * 用戶賬號(hào) */ @TableField("USER_ACCOUNT") private String userAccount; /** * 用戶地址 */ @TableField("USER_ADDRESS") private String userAddress; /** * 用戶密碼 */ @TableField("USER_PASSWORD") private String userPassword; /** * 用戶城市 */ @TableField("USER_CITY") private String userCity; /** * 用戶狀態(tài) */ @TableField("USER_STATUS") private String userStatus; /** * 用戶區(qū)縣 */ @TableField("USER_SEAT") private String userSeat; }
攔截器
Mybatis-Plus
有個(gè)攔截器接口com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor
,但發(fā)現(xiàn)這個(gè)接口有一些不足
- 必須構(gòu)建一個(gè)
MybatisPlusInterceptor
這樣的Bean - 并調(diào)用這個(gè)
Bean
的addInnerInterceptor
方法,把所有的InnerInterceptor
加入進(jìn)去,才能生效 InnerInterceptor
只有before
攔截,缺省after
攔截。加密可以在before
里面完成,但解密需要在after
里面完成,所以這個(gè)InnerInterceptor
不能滿足我們的要求
所以繼續(xù)研究源碼,發(fā)現(xiàn)Mybatis
有個(gè)org.apache.ibatis.plugin.Interceptor
接口,這個(gè)接口能滿足我對(duì)自動(dòng)加解密的所有訴求
- 首先,實(shí)現(xiàn)
Interceptor
接口,只要注冊(cè)成為Spring
容器的Bean
,攔截器就能生效 - 可以更加靈活的在
before
和after
之間插入自己的邏輯
加密攔截器
創(chuàng)建名為EncryptInterceptor
的加密攔截器,對(duì)update
操作進(jìn)行攔截,對(duì)帶@EncryptField
注解的字段進(jìn)行加密處理,無(wú)論是save
方法還是saveBatch
方法都會(huì)被成功攔截到。
package com.wen3.demo.mybatisplus.encrypt.interceptor; import com.wen3.demo.mybatisplus.encrypt.annotation.EncryptField; import com.wen3.demo.mybatisplus.encrypt.util.FieldEncryptUtil; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Signature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.Objects; /** * 對(duì)update操作進(jìn)行攔截,對(duì){@link EncryptField}字段進(jìn)行加密處理; * 無(wú)論是save方法還是saveBatch方法都會(huì)被成功攔截; */ @Slf4j @Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) }) @Component public class EncryptInterceptor implements Interceptor { private static final String METHOD = "update"; @Setter(onMethod_ = {@Autowired}) private FieldEncryptUtil fieldEncryptUtil; @Override public Object intercept(Invocation invocation) throws Throwable { if(!StringUtils.equals(METHOD, invocation.getMethod().getName())) { return invocation.proceed(); } // 根據(jù)update攔截規(guī)則,第0個(gè)參數(shù)一定是MappedStatement,第1個(gè)參數(shù)是需要進(jìn)行判斷的參數(shù) Object param = invocation.getArgs()[1]; if(Objects.isNull(param)) { return invocation.proceed(); } // 加密處理 fieldEncryptUtil.encrypt(param); return invocation.proceed(); } }
解密攔截器
創(chuàng)建名為DecryptInterceptor
的加密攔截器,對(duì)query
操作進(jìn)行攔截,對(duì)帶@EncryptField
注解的字段進(jìn)行解密處理,無(wú)論是返回單個(gè)對(duì)象,還是對(duì)象的集合,都會(huì)被攔截到。
package com.wen3.demo.mybatisplus.encrypt.interceptor; import cn.hutool.core.util.ClassUtil; import com.wen3.demo.mybatisplus.encrypt.annotation.EncryptField; import com.wen3.demo.mybatisplus.encrypt.util.FieldEncryptUtil; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.executor.resultset.ResultSetHandler; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Signature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.sql.Statement; import java.util.Collection; /** * 對(duì)query操作進(jìn)行攔截,對(duì){@link EncryptField}字段進(jìn)行解密處理; */ @Slf4j @Intercepts({ @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = Statement.class) }) @Component public class DecryptInterceptor implements Interceptor { private static final String METHOD = "query"; @Setter(onMethod_ = {@Autowired}) private FieldEncryptUtil fieldEncryptUtil; @SuppressWarnings("rawtypes") @Override public Object intercept(Invocation invocation) throws Throwable { Object result = invocation.proceed(); // 解密處理 // 經(jīng)過測(cè)試發(fā)現(xiàn),無(wú)論是返回單個(gè)對(duì)象還是集合,result都是ArrayList類型 if(ClassUtil.isAssignable(Collection.class, result.getClass())) { fieldEncryptUtil.decrypt((Collection) result); } else { fieldEncryptUtil.decrypt(result); } return result; } }
加解密工具類
由于加密和解密絕大部分的邏輯是相似的,不同的地方在于
- 加密需要通過反射處理的對(duì)象,是在
SQL
執(zhí)行前,是Invocation
對(duì)象的參數(shù)列表中下標(biāo)為1
的參數(shù);而解決需要通過反射處理的對(duì)象,是在SQL
執(zhí)行后,對(duì)執(zhí)行結(jié)果對(duì)象進(jìn)行解密處理。 - 一個(gè)是獲取到字段值進(jìn)行加密,一個(gè)是獲取到字段值進(jìn)行解密
于是把加解密邏輯抽象成一個(gè)工具類,把差異的部分做為參數(shù)傳入
package com.wen3.demo.mybatisplus.encrypt.util; import cn.hutool.core.util.ClassUtil; import cn.hutool.core.util.ReflectUtil; import com.wen3.demo.mybatisplus.encrypt.annotation.EncryptField; import com.wen3.demo.mybatisplus.encrypt.service.FieldEncryptService; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.reflect.FieldUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.lang.reflect.Field; import java.util.Collection; import java.util.List; import java.util.Objects; /** * 加解密工具類 */ @Slf4j @Component public class FieldEncryptUtil { @Setter(onMethod_ = {@Autowired}) private FieldEncryptService fieldEncryptService; /**對(duì)EncryptField注解進(jìn)行加密處理*/ public void encrypt(Object obj) { if(ClassUtil.isPrimitiveWrapper(obj.getClass())) { return; } encryptOrDecrypt(obj, true); } /**對(duì)EncryptField注解進(jìn)行解密處理*/ public void decrypt(Object obj) { encryptOrDecrypt(obj, false); } /**對(duì)EncryptField注解進(jìn)行解密處理*/ public void decrypt(Collection list) { if(CollectionUtils.isEmpty(list)) { return; } list.forEach(this::decrypt); } /**對(duì)EncryptField注解進(jìn)行加解密處理*/ private void encryptOrDecrypt(Object obj, boolean encrypt) { // 根據(jù)update攔截規(guī)則,第0個(gè)參數(shù)一定是MappedStatement,第1個(gè)參數(shù)是需要進(jìn)行判斷的參數(shù) if(Objects.isNull(obj)) { return; } // 獲取所有帶加密注解的字段 List<Field> encryptFields = null; // 判斷類上面是否有加密注解 EncryptField encryptField = AnnotationUtils.findAnnotation(obj.getClass(), EncryptField.class); if(Objects.nonNull(encryptField)) { // 如果類上有加密注解,則所有字段都需要加密 encryptFields = FieldUtils.getAllFieldsList(obj.getClass()); } else { encryptFields = FieldUtils.getFieldsListWithAnnotation(obj.getClass(), EncryptField.class); } // 沒有字段需要加密,則跳過 if(CollectionUtils.isEmpty(encryptFields)) { return; } encryptFields.forEach(f->{ // 只支持String類型的加密 if(!ClassUtil.isAssignable(String.class, f.getType())) { return; } String oldValue = (String) ReflectUtil.getFieldValue(obj, f); if(StringUtils.isBlank(oldValue)) { return; } String logText = null, newValue = null; if(encrypt) { logText = "encrypt"; newValue = fieldEncryptService.encrypt(oldValue); } else { logText = "decrypt"; newValue = fieldEncryptService.decrypt(oldValue); } log.info("{} success[{}=>{}]. before:{}, after:{}", logText, f.getDeclaringClass().getName(), f.getName(), oldValue, newValue); ReflectUtil.setFieldValue(obj, f, newValue); }); } }
加解密算法
Mybatis-Plus
自帶了一個(gè)AES
加解密算法的工具,我們只需要提供一個(gè)加密key
,然后就可以完成一個(gè)加解密的業(yè)務(wù)處理了。
- 先定義一個(gè)加解密接口
package com.wen3.demo.mybatisplus.encrypt.service; /** * 數(shù)據(jù)加解密接口 */ public interface FieldEncryptService { /**對(duì)數(shù)據(jù)進(jìn)行加密*/ String encrypt(String value); /**對(duì)數(shù)據(jù)進(jìn)行解密*/ String decrypt(String value); /**判斷數(shù)據(jù)是否憶加密*/ default boolean isEncrypt(String value) { return false; } }
- 然后實(shí)現(xiàn)一個(gè)默認(rèn)的加解密實(shí)現(xiàn)類
package com.wen3.demo.mybatisplus.encrypt.service.impl; import cn.hutool.core.util.ClassUtil; import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException; import com.baomidou.mybatisplus.core.toolkit.AES; import com.wen3.demo.mybatisplus.encrypt.service.FieldEncryptService; import org.springframework.stereotype.Component; import javax.crypto.IllegalBlockSizeException; /** * 使用Mybatis-Plus自帶的AES加解密 */ @Component public class DefaultFieldEncryptService implements FieldEncryptService { private static final String ENCRYPT_KEY = "abcdefghijklmnop"; @Override public String encrypt(String value) { if(isEncrypt(value)) { return value; } return AES.encrypt(value, ENCRYPT_KEY); } @Override public String decrypt(String value) { return AES.decrypt(value, ENCRYPT_KEY); } @Override public boolean isEncrypt(String value) { // 判斷是否已加密 try { // 解密成功,說(shuō)明已加密 decrypt(value); return true; } catch (MybatisPlusException e) { if(ClassUtil.isAssignable(IllegalBlockSizeException.class, e.getCause().getClass())) { return false; } throw e; } } }
自動(dòng)加解密單元測(cè)試
package com.wen3.demo.mybatisplus.service; import cn.hutool.core.util.RandomUtil; import com.wen3.demo.mybatisplus.MybatisPlusSpringbootTestBase; import com.wen3.demo.mybatisplus.encrypt.service.FieldEncryptService; import com.wen3.demo.mybatisplus.po.UserPo; import jakarta.annotation.Resource; import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.Test; import java.util.Collections; import java.util.List; import java.util.Map; class UserServiceTest extends MybatisPlusSpringbootTestBase { @Resource private UserService userService; @Resource private FieldEncryptService fieldEncryptService; @Test void save() { UserPo userPo = new UserPo(); String originalValue = RandomStringUtils.randomAlphabetic(16); String encryptValue = fieldEncryptService.encrypt(originalValue); userPo.setUserEmail(originalValue); userPo.setUserName(RandomStringUtils.randomAlphabetic(16)); boolean testResult = userService.save(userPo); assertTrue(testResult); assertNotEquals(originalValue, userPo.getUserEmail()); assertEquals(encryptValue, userPo.getUserEmail()); // 測(cè)試解密: 返回單個(gè)對(duì)象 UserPo userPoQuery = userService.getById(userPo.getUserId()); assertEquals(originalValue, userPoQuery.getUserEmail()); // 測(cè)試解密: 返回List List<UserPo> userPoList = userService.listByEmail(encryptValue); assertEquals(originalValue, userPoList.get(0).getUserEmail()); // 測(cè)試saveBatch方法也會(huì)被攔截加密 userPo.setUserId(null); testResult = userService.save(Collections.singletonList(userPo)); assertTrue(testResult); assertNotEquals(originalValue, userPo.getUserEmail()); assertEquals(encryptValue, userPo.getUserEmail()); } }
單元測(cè)試運(yùn)行截圖
以上就是Mybatis-Plus根據(jù)自定義注解實(shí)現(xiàn)自動(dòng)加解密的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于Mybatis-Plus自定義注解加解密的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springboot的yml配置文件通過db2的方式整合mysql的教程
這篇文章主要介紹了springboot的yml配置文件通過db2的方式整合mysql的教程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09spring boot配置ssl實(shí)現(xiàn)HTTPS的方法
這篇文章主要介紹了spring boot配置ssl實(shí)現(xiàn)HTTPS的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2019-03-03SpringCloud微服務(wù)熔斷器Hystrix使用詳解
這篇文章主要介紹了Spring Cloud Hyxtrix的基本使用,它是Spring Cloud中集成的一個(gè)組件,在整個(gè)生態(tài)中主要為我們提供服務(wù)隔離,服務(wù)熔斷,服務(wù)降級(jí)功能,本文給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07在Java 8中將List轉(zhuǎn)換為Map對(duì)象方法
這篇文章主要介紹了在Java 8中將List轉(zhuǎn)換為Map對(duì)象方法,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-11-11springboot使用JPA時(shí)間類型進(jìn)行模糊查詢的方法
這篇文章主要介紹了springboot使用JPA時(shí)間類型進(jìn)行模糊查詢的方法,需要的朋友可以參考下2018-03-03Java?Calendar類使用之日期和時(shí)間處理指南
這篇文章主要給大家介紹了關(guān)于Java?Calendar類使用之日期和時(shí)間處理指南的相關(guān)資料,Calendar類是Java中用于處理日期和時(shí)間的抽象類,它提供了一種獨(dú)立于特定日歷系統(tǒng)的方式來(lái)處理日期和時(shí)間,需要的朋友可以參考下2023-12-12Spring?Cloud?Gateway整合sentinel?實(shí)現(xiàn)流控熔斷的問題
本文給大家介紹下?spring?cloud?gateway?如何整合?sentinel實(shí)現(xiàn)流控熔斷,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友一起看看吧2022-02-02