目錄
- 前言
- 實現
- 自定義注解
- AES對稱加密工具類
- 創建攔截器
- 加密攔截器
- 解密攔截器
- 驗證
- 創建實體類
- 數據寫入與查詢
- 加密字段參與查詢
- 不生效情況
前言
通過Mybatis提供的攔截器,在新增、修改時對特定的敏感字段進行加密存儲,查詢時自動進行解密操作,減少業務層面的代碼邏輯;
加密存儲意義:
- 防止數據泄露:即使數據庫被非法訪問或泄露,加密數據也無法被直接利用
- 保護個人隱私:如身份證號、手機號、住址等PII(個人身份信息)數據
- 保障財務安全:加密銀行卡號、支付密碼等金融信息
核心邏輯:
- 自定義注解,對需要進行加密存儲的使用注解進行標注;
- 構建AES對稱加密工具類;
- 實現Mybatis攔截器,通過反射獲取當前實體類的字段是否需要進行加解密;
實現
自定義注解
通過自定義@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個字節*/private final static String KEY = "1234567812345678";/*** 設置偏移量,IV值任意16個字節*/private final static String IV = "1122334455667788";/*** 對稱加密數據** @return : 密文* @throws Exception*/public static String encryptBySymmetry(String input) {try {// CBC模式String transformation = ALGORITHM + CBC;// 獲取加密對象Cipher cipher = Cipher.getInstance(transformation);// 創建加密規則// 第一個參數key的字節// 第二個參數表示加密算法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());// 輸出加密后的數據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);// 指定密鑰規則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);}}
}
創建攔截器
- 加密攔截器: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 {@Overridepublic 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);}}/*** 默認配置,否則當前攔截器不會加入攔截器鏈*/@Overridepublic Object plugin(Object o) {return Plugin.wrap(o, this);}}
解密攔截器
將查詢的數據,返回為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 {@Overridepublic Object intercept(Invocation invocation) throws Throwable {Object resultObject = invocation.proceed();try {if (Objects.isNull(resultObject)) {return null;}// 查詢列表數據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 {// 查詢單個數據Class<?> objectClass = resultObject.getClass();boolean encryptDBBean = objectClass.isAnnotationPresent(EncryptDBBean.class);if (encryptDBBean) {// 解密處理decrypt(resultObject);}}return resultObject;} catch (Exception e) {throw new RuntimeException("字段解密失敗!", e);}}@Overridepublic 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));}}}}
}
驗證
創建實體類
創建實體類,并且使用加密注解@EncryptDBBean
、@EncryptDBColumn
進行標注,此處以手機號
為例;
@Data
@TableName("sys_user_info")
@EncryptDBBean
public class TestEntity {/*** 用戶id*/@TableId("id")private Long id;/*** 用戶名稱*/private String name;/*** 手機號*/@EncryptDBColumnprivate String mobile;
}
數據寫入與查詢
對數據的操作使用偽代碼進行表示
TestEntity entity = new TestEntity();
entity.setId(1L);
entity.setName("測試");
entity.setMobile("166xxxx8888");
// 插入數據
entityService.insert(entity);
// 更新數據
entity.setMobile("166xxxx7777");
entityService.updateById(entity);// 列表查詢
List<TestEntity> list = testService.list();
效果:
- insert和update后的數據,在數據庫是加密字符串存儲的形式;
- list方法查詢的數據,將明文進行顯示;
加密字段參與查詢
如果是加密字段進行條件查詢時,需要自行將查詢參數進行加密處理,因為數據庫是存儲的密文,所以查詢時也需要使用密文進行匹配,比如:要查詢mobile=111
的數據
// 偽代碼
// 獲取前端傳入的查詢條件
String mobile = "111"
// 手動加密
mobile = DBAESUtils.decryptBySymmetry(mobile );
testService.selectByMobile(mobile);
不生效情況
1、在通過LambdaQueryWrapper
獲取QueryWrapper
方式查詢時,攔截器無法獲取自定義注解對象,需要手動對查詢的字段進行加密,比如:
如果是 通過自定義的xml查詢,如果入參有加密注解,那么會自動對字段進行加密處理 testMapper.listTest(testEntity)
LambdaQueryWrapper<TestEntity> wrapper = new LambdaQueryWrapper<>();
String mobile = test.getMobile();
if (mobile != null) {// mobile在數據庫中加密儲存,此處需要手動進行加密mobile = DBAESUtils.encryptBySymmetry(mobile);
}
wrapper.eq(StringUtils.isNotBlank(test.getMobile()), TestEntity::getMobile, mobile);
List<TestEntity> testEntities = testMapper.selectList(wrapper);
2、使用Mybatis提供的selectOne或者getOne方法查詢時,無法對響應的數據進行解密,需要手動進行處理,比如:
如果是 通過自定義的xml查詢,無論多少條數據都會對數據進行解密,testMapper.selectXmlById(Long id)
TestEntity one = testService.getOne(new QueryWrapper<>(), false);
// mobile在數據庫中加密儲存,此處需要手動進行解密
one.setMobile(DBAESUtils.decryptBySymmetry(one.getMobile()));