Mybatis-Plus根據(jù)自定義注解實現(xiàn)自動加解密的示例代碼
背景
我們把數(shù)據(jù)存到數(shù)據(jù)庫的時候,有些敏感字段是需要加密的,從數(shù)據(jù)庫查出來再進行解密。如果存在多張表或者多個地方需要對部分字段進行加解密操作,每個地方都手寫一次加解密的動作,顯然不是最好的選擇。如果我們使用的是Mybatis框架,那就跟著一起探索下如何使用框架的攔截器功能實現(xiàn)自動加解密吧。
定義一個自定義注解
我們需要一個注解,只要實體類的屬性加上這個注解,那么就對這個屬性進行自動加解密。我們把這個注解定義靈活一點,不僅可以放在屬性上,還可以放到類上,如果在類上使用這個注解,代表這個類的所有屬性都進行自動加密。
/**
* 加密字段
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.TYPE})
public @interface EncryptField {
}
定義實體類
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;
/**
* 用戶賬號
*/
@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有個攔截器接口com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor,但發(fā)現(xiàn)這個接口有一些不足
- 必須構建一個
MybatisPlusInterceptor這樣的Bean - 并調(diào)用這個
Bean的addInnerInterceptor方法,把所有的InnerInterceptor加入進去,才能生效 InnerInterceptor只有before攔截,缺省after攔截。加密可以在before里面完成,但解密需要在after里面完成,所以這個InnerInterceptor不能滿足我們的要求
所以繼續(xù)研究源碼,發(fā)現(xiàn)Mybatis有個org.apache.ibatis.plugin.Interceptor接口,這個接口能滿足我對自動加解密的所有訴求
- 首先,實現(xiàn)
Interceptor接口,只要注冊成為Spring容器的Bean,攔截器就能生效 - 可以更加靈活的在
before和after之間插入自己的邏輯
加密攔截器
創(chuàng)建名為EncryptInterceptor的加密攔截器,對update操作進行攔截,對帶@EncryptField注解的字段進行加密處理,無論是save方法還是saveBatch方法都會被成功攔截到。
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;
/**
* 對update操作進行攔截,對{@link EncryptField}字段進行加密處理;
* 無論是save方法還是saveBatch方法都會被成功攔截;
*/
@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個參數(shù)一定是MappedStatement,第1個參數(shù)是需要進行判斷的參數(shù)
Object param = invocation.getArgs()[1];
if(Objects.isNull(param)) {
return invocation.proceed();
}
// 加密處理
fieldEncryptUtil.encrypt(param);
return invocation.proceed();
}
}
解密攔截器
創(chuàng)建名為DecryptInterceptor的加密攔截器,對query操作進行攔截,對帶@EncryptField注解的字段進行解密處理,無論是返回單個對象,還是對象的集合,都會被攔截到。
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;
/**
* 對query操作進行攔截,對{@link EncryptField}字段進行解密處理;
*/
@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)過測試發(fā)現(xiàn),無論是返回單個對象還是集合,result都是ArrayList類型
if(ClassUtil.isAssignable(Collection.class, result.getClass())) {
fieldEncryptUtil.decrypt((Collection) result);
} else {
fieldEncryptUtil.decrypt(result);
}
return result;
}
}
加解密工具類
由于加密和解密絕大部分的邏輯是相似的,不同的地方在于
- 加密需要通過反射處理的對象,是在
SQL執(zhí)行前,是Invocation對象的參數(shù)列表中下標為1的參數(shù);而解決需要通過反射處理的對象,是在SQL執(zhí)行后,對執(zhí)行結(jié)果對象進行解密處理。 - 一個是獲取到字段值進行加密,一個是獲取到字段值進行解密
于是把加解密邏輯抽象成一個工具類,把差異的部分做為參數(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;
/**對EncryptField注解進行加密處理*/
public void encrypt(Object obj) {
if(ClassUtil.isPrimitiveWrapper(obj.getClass())) {
return;
}
encryptOrDecrypt(obj, true);
}
/**對EncryptField注解進行解密處理*/
public void decrypt(Object obj) {
encryptOrDecrypt(obj, false);
}
/**對EncryptField注解進行解密處理*/
public void decrypt(Collection list) {
if(CollectionUtils.isEmpty(list)) {
return;
}
list.forEach(this::decrypt);
}
/**對EncryptField注解進行加解密處理*/
private void encryptOrDecrypt(Object obj, boolean encrypt) {
// 根據(jù)update攔截規(guī)則,第0個參數(shù)一定是MappedStatement,第1個參數(shù)是需要進行判斷的參數(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自帶了一個AES加解密算法的工具,我們只需要提供一個加密key,然后就可以完成一個加解密的業(yè)務處理了。
- 先定義一個加解密接口
package com.wen3.demo.mybatisplus.encrypt.service;
/**
* 數(shù)據(jù)加解密接口
*/
public interface FieldEncryptService {
/**對數(shù)據(jù)進行加密*/
String encrypt(String value);
/**對數(shù)據(jù)進行解密*/
String decrypt(String value);
/**判斷數(shù)據(jù)是否憶加密*/
default boolean isEncrypt(String value) {
return false;
}
}
- 然后實現(xiàn)一個默認的加解密實現(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 {
// 解密成功,說明已加密
decrypt(value);
return true;
} catch (MybatisPlusException e) {
if(ClassUtil.isAssignable(IllegalBlockSizeException.class, e.getCause().getClass())) {
return false;
}
throw e;
}
}
}
自動加解密單元測試
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());
// 測試解密: 返回單個對象
UserPo userPoQuery = userService.getById(userPo.getUserId());
assertEquals(originalValue, userPoQuery.getUserEmail());
// 測試解密: 返回List
List<UserPo> userPoList = userService.listByEmail(encryptValue);
assertEquals(originalValue, userPoList.get(0).getUserEmail());
// 測試saveBatch方法也會被攔截加密
userPo.setUserId(null);
testResult = userService.save(Collections.singletonList(userPo));
assertTrue(testResult);
assertNotEquals(originalValue, userPo.getUserEmail());
assertEquals(encryptValue, userPo.getUserEmail());
}
}
單元測試運行截圖

以上就是Mybatis-Plus根據(jù)自定義注解實現(xiàn)自動加解密的示例代碼的詳細內(nèi)容,更多關于Mybatis-Plus自定義注解加解密的資料請關注腳本之家其它相關文章!
相關文章
springboot的yml配置文件通過db2的方式整合mysql的教程
這篇文章主要介紹了springboot的yml配置文件通過db2的方式整合mysql的教程,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09
spring boot配置ssl實現(xiàn)HTTPS的方法
這篇文章主要介紹了spring boot配置ssl實現(xiàn)HTTPS的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-03-03
在Java 8中將List轉(zhuǎn)換為Map對象方法
這篇文章主要介紹了在Java 8中將List轉(zhuǎn)換為Map對象方法,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2018-11-11
Spring?Cloud?Gateway整合sentinel?實現(xiàn)流控熔斷的問題
本文給大家介紹下?spring?cloud?gateway?如何整合?sentinel實現(xiàn)流控熔斷,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友一起看看吧2022-02-02

