SpringBoot+Mybatis通過自定義注解實現(xiàn)字段加密存儲方式
前言
通過Mybatis提供的攔截器,在新增、修改時對特定的敏感字段進行加密存儲,查詢時自動進行解密操作,減少業(yè)務層面的代碼邏輯;
加密存儲意義:
- 防止數(shù)據(jù)泄露:即使數(shù)據(jù)庫被非法訪問或泄露,加密數(shù)據(jù)也無法被直接利用
- 保護個人隱私:如身份證號、手機號、住址等PII(個人身份信息)數(shù)據(jù)
- 保障財務安全:加密銀行卡號、支付密碼等金融信息
核心邏輯:
- 自定義注解,對需要進行加密存儲的使用注解進行標注;
- 構(gòu)建AES對稱加密工具類;
- 實現(xiàn)Mybatis攔截器,通過反射獲取當前實體類的字段是否需要進行加解密;
實現(xiàn)
自定義注解
通過自定義@EncryptDBBean與@EncryptDBColumn標識某個DO實體類的某些字段需要進行加解密處理;
- EncryptDBBean:作用在類上
- EncryptDBColumn:作用在字段上
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptDBBean {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface EncryptDBColumn {
}
AES對稱加密工具類
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class DBAESUtils {
/**
* 設置為CBC加密模式,默認情況下ECB比CBC更高效
*/
private final static String CBC = "/CBC/PKCS5Padding";
private final static String ALGORITHM = "AES";
/**
* 定義密鑰Key,AES加密算法,key的大小必須是16個字節(jié)
*/
private final static String KEY = "1234567812345678";
/**
* 設置偏移量,IV值任意16個字節(jié)
*/
private final static String IV = "1122334455667788";
/**
* 對稱加密數(shù)據(jù)
*
* @return : 密文
* @throws Exception
*/
public static String encryptBySymmetry(String input) {
try {
// CBC模式
String transformation = ALGORITHM + CBC;
// 獲取加密對象
Cipher cipher = Cipher.getInstance(transformation);
// 創(chuàng)建加密規(guī)則
// 第一個參數(shù)key的字節(jié)
// 第二個參數(shù)表示加密算法
SecretKeySpec sks = new SecretKeySpec(KEY.getBytes(), ALGORITHM);
// ENCRYPT_MODE:加密模式
// DECRYPT_MODE: 解密模式
// 使用CBC模式
IvParameterSpec iv = new IvParameterSpec(IV.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, sks, iv);
// 加密
byte[] bytes = cipher.doFinal(input.getBytes());
// 輸出加密后的數(shù)據(jù)
return Base64.getEncoder().encodeToString(bytes);
} catch (Exception e) {
throw new RuntimeException("加密失敗!", e);
}
}
/**
* 對稱解密
*
* @param input : 密文
* @throws Exception
* @return: 原文
*/
public static String decryptBySymmetry(String input) {
try {
// CBC模式
String transformation = ALGORITHM + CBC;
// 1,獲取Cipher對象
Cipher cipher = Cipher.getInstance(transformation);
// 指定密鑰規(guī)則
SecretKeySpec sks = new SecretKeySpec(KEY.getBytes(), ALGORITHM);
// 使用CBC模式
IvParameterSpec iv = new IvParameterSpec(IV.getBytes());
cipher.init(Cipher.DECRYPT_MODE, sks, iv);
// 3. 解密,上面使用的base64編碼,下面直接用密文
byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(input));
// 因為是明文,所以直接返回
return new String(bytes);
} catch (Exception e) {
throw new RuntimeException("解密失??!", e);
}
}
}
創(chuàng)建攔截器
- 加密攔截器:EncryptInterceptor
- 解密攔截器:DecryptInterceptor
加密攔截器
在新增或者更新時,通過攔截對被注解標識的字段進行加密存儲處理;
import com.lhz.demo.annotation.EncryptDBBean;
import com.lhz.demo.annotation.EncryptDBColumn;
import com.lhz.demo.utils.DBAESUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.util.*;
@Slf4j
@Component
@Intercepts({
@Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}),
})
public class EncryptInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
parameterField.setAccessible(true);
Object parameterObject = parameterField.get(parameterHandler);
if (parameterObject != null) {
Set<Object> objectList = new HashSet<>();
if (parameterObject instanceof Map<?, ?>) {
Collection<?> values = ((Map<?, ?>) parameterObject).values();
objectList.addAll(values);
} else {
objectList.add(parameterObject);
}
for (Object o1 : objectList) {
Class<?> o1Class = o1.getClass();
// 實體類是否存在 加密注解
boolean encryptDBBean = o1Class.isAnnotationPresent(EncryptDBBean.class);
if (encryptDBBean) {
//取出當前當前類所有字段,傳入加密方法
Field[] declaredFields = o1Class.getDeclaredFields();
// 便利字段,是否存在加密注解,并且進行加密處理
for (Field field : declaredFields) {
//取出所有被EncryptDecryptField注解的字段
boolean annotationPresent = field.isAnnotationPresent(EncryptDBColumn.class);
if (annotationPresent) {
field.setAccessible(true);
Object object = field.get(o1);
if (object != null) {
String value = object.toString();
//加密 這里我使用自定義的AES加密工具
field.set(o1, DBAESUtils.encryptBySymmetry(value));
}
}
}
}
}
}
return invocation.proceed();
} catch (Exception e) {
throw new RuntimeException("字段加密失??!", e);
}
}
/**
* 默認配置,否則當前攔截器不會加入攔截器鏈
*/
@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}
}
解密攔截器
將查詢的數(shù)據(jù),返回為DO實體類時,對被注解標識的字段進行解密處理
import com.lhz.demo.annotation.EncryptDBBean;
import com.lhz.demo.annotation.EncryptDBColumn;
import com.lhz.demo.utils.DBAESUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})})
@Slf4j
@Component
public class DecryptInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object resultObject = invocation.proceed();
try {
if (Objects.isNull(resultObject)) {
return null;
}
// 查詢列表數(shù)據(jù)
if (resultObject instanceof ArrayList) {
List list = (ArrayList) resultObject;
if (!CollectionUtils.isEmpty(list)) {
for (Object result : list) {
Class<?> objectClass = result.getClass();
boolean encryptDBBean = objectClass.isAnnotationPresent(EncryptDBBean.class);
if (encryptDBBean) {
// 解密處理
decrypt(result);
}
}
}
} else {
// 查詢單個數(shù)據(jù)
Class<?> objectClass = resultObject.getClass();
boolean encryptDBBean = objectClass.isAnnotationPresent(EncryptDBBean.class);
if (encryptDBBean) {
// 解密處理
decrypt(resultObject);
}
}
return resultObject;
} catch (Exception e) {
throw new RuntimeException("字段解密失??!", e);
}
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}
public <T> void decrypt(T result) throws Exception {
//取出resultType的類
Class<?> resultClass = result.getClass();
Field[] declaredFields = resultClass.getDeclaredFields();
for (Field field : declaredFields) {
boolean annotationPresent = field.isAnnotationPresent(EncryptDBColumn.class);
if (annotationPresent) {
field.setAccessible(true);
Object object = field.get(result);
if (object != null) {
String value = object.toString();
//對注解的字段進行逐一解密
field.set(result, DBAESUtils.decryptBySymmetry(value));
}
}
}
}
}
驗證
創(chuàng)建實體類
創(chuàng)建實體類,并且使用加密注解@EncryptDBBean、@EncryptDBColumn進行標注,此處以手機號為例;
@Data
@TableName("sys_user_info")
@EncryptDBBean
public class TestEntity {
/**
* 用戶id
*/
@TableId("id")
private Long id;
/**
* 用戶名稱
*/
private String name;
/**
* 手機號
*/
@EncryptDBColumn
private String mobile;
}
數(shù)據(jù)寫入與查詢
對數(shù)據(jù)的操作使用偽代碼進行表示
TestEntity entity = new TestEntity();
entity.setId(1L);
entity.setName("測試");
entity.setMobile("166xxxx8888");
// 插入數(shù)據(jù)
entityService.insert(entity);
// 更新數(shù)據(jù)
entity.setMobile("166xxxx7777");
entityService.updateById(entity);
// 列表查詢
List<TestEntity> list = testService.list();
效果:
- insert和update后的數(shù)據(jù),在數(shù)據(jù)庫是加密字符串存儲的形式;
- list方法查詢的數(shù)據(jù),將明文進行顯示;
加密字段參與查詢
如果是加密字段進行條件查詢時,需要自行將查詢參數(shù)進行加密處理,因為數(shù)據(jù)庫是存儲的密文,所以查詢時也需要使用密文進行匹配,比如:要查詢mobile=111的數(shù)據(jù)
// 偽代碼 // 獲取前端傳入的查詢條件 String mobile = "111" // 手動加密 mobile = DBAESUtils.decryptBySymmetry(mobile ); testService.selectByMobile(mobile);
不生效情況
1、在通過LambdaQueryWrapper獲取QueryWrapper方式查詢時,攔截器無法獲取自定義注解對象,需要手動對查詢的字段進行加密,比如:
如果是 通過自定義的xml查詢,如果入?yún)⒂屑用茏⒔猓敲磿詣訉ψ侄芜M行加密處理 testMapper.listTest(testEntity)
LambdaQueryWrapper<TestEntity> wrapper = new LambdaQueryWrapper<>();
String mobile = test.getMobile();
if (mobile != null) {
// mobile在數(shù)據(jù)庫中加密儲存,此處需要手動進行加密
mobile = DBAESUtils.encryptBySymmetry(mobile);
}
wrapper.eq(StringUtils.isNotBlank(test.getMobile()), TestEntity::getMobile, mobile);
List<TestEntity> testEntities = testMapper.selectList(wrapper);
2、使用Mybatis提供的selectOne或者getOne方法查詢時,無法對響應的數(shù)據(jù)進行解密,需要手動進行處理,比如:
如果是 通過自定義的xml查詢,無論多少條數(shù)據(jù)都會對數(shù)據(jù)進行解密,testMapper.selectXmlById(Long id)
TestEntity one = testService.getOne(new QueryWrapper<>(), false); // mobile在數(shù)據(jù)庫中加密儲存,此處需要手動進行解密 one.setMobile(DBAESUtils.decryptBySymmetry(one.getMobile()));
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
2022年最新java?8?(?jdk1.8u321)安裝圖文教程
這篇文章主要介紹了2022年最新java?8?(?jdk1.8u321)安裝圖文教程,截止2022年1月,官方出的jdk1.8目前已更新到8u321的版本,本文通過圖文并茂的形式給大家介紹安裝過程,需要的朋友可以參考下2022-08-08
Java Annotation注解相關原理代碼總結(jié)
這篇文章主要介紹了Java Annotation注解相關原理代碼總結(jié),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-07-07
MyBatis樹形結(jié)構(gòu)查詢的實現(xiàn)示例
本文介紹了一種使用MyBatis進行樹形結(jié)構(gòu)查詢的方法,通過構(gòu)造臨時表和內(nèi)連接,實現(xiàn)了不同層級數(shù)據(jù)的一次性查詢及映射,感興趣的可以了解一下2025-11-11
Java?+?Selenium?+?OpenCV解決自動化測試中的滑塊驗證問題
OpenCV是一個基于Apache2.0許可(開源)發(fā)行的跨平臺計算機視覺和機器學習軟件庫,可以運行在Linux、Windows、Android和Mac?OS操作系統(tǒng)上,這篇文章主要介紹了Java?+?Selenium?+?OpenCV解決自動化測試中的滑塊驗證,需要的朋友可以參考下2022-07-07
解決redisTemplate中l(wèi)eftPushAll隱性bug的問題
這篇文章主要介紹了解決redisTemplate中l(wèi)eftPushAll隱性bug的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02
JavaScript Date時間對象的常用操作方法總結(jié)
本文介紹了JavaScript中Date對象的常用操作方法,包括創(chuàng)建、獲取、設置本地及UTC時間等,幫助開發(fā)者靈活處理日期與時間的相關需求,需要的朋友可以參考下2025-10-10
使用@Cacheable注解Redis時Redis宕機或其他原因連不上繼續(xù)調(diào)用原方法的解決方案
在Spring Boot應用中,我們經(jīng)常使用??@Cacheable??注解來緩存數(shù)據(jù),以提高應用的性能,本文將探討如何在Redis不可用時,讓??@Cacheable??注解繼續(xù)調(diào)用原方法,確保服務的可用性和穩(wěn)定性,感興趣的朋友一起看看吧2025-05-05
SpringBoot如何使用applicationContext.xml配置文件
這篇文章主要介紹了SpringBoot使用applicationContext.xml配置文件,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-06-06

