SpringBoot+Mybatis通过自定义注解实现字段加密存储
😊 @ 作者: 一恍过去
💖 @ 主页: https://blog.csdn.net/zhuocailing3390
🎊 @ 社区: Java技术栈交流
🎉 @ 主题: SpringBoot+Mybatis实现字段加密
⏱️ @ 创作时间: 2025年04月29日
目录
- 前言
- 实现
- 自定义注解
- 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()));