SpringBoot+Mybatis通过自定义注解实现字段加密存储方式

 更新时间:2026年03月02日 09:44:00   作者:一恍过去  
本文介绍了如何使用Mybatis拦截器对敏感字段进行加密存储和查询时的自动解密操作,通过自定义注解和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 {

    @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);
    }

}

解密拦截器

将查询的数据,返回为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;
            }
            // 查询列表数据
            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);
        }

    }

    @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));
                }
            }
        }
    }
}

验证

创建实体类

创建实体类,并且使用加密注解@EncryptDBBean@EncryptDBColumn进行标注,此处以手机号为例;

@Data
@TableName("sys_user_info")
@EncryptDBBean
public class TestEntity {
    /**
     * 用户id
     */
    @TableId("id")
    private Long id;

    /**
     * 用户名称
     */
    private String name;

    /**
     * 手机号
     */
    @EncryptDBColumn
    private 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()));

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • 解决Maven项目本地公共common包缓存问题

    解决Maven项目本地公共common包缓存问题

    这篇文章主要介绍了解决Maven项目本地公共common包缓存问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09
  • Java代理模式的深入了解

    Java代理模式的深入了解

    这篇文章主要为大家介绍了Java代理模式,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-01-01
  • mybatis-plus多表关联查询功能的实现

    mybatis-plus多表关联查询功能的实现

    本文给大家介绍mybatis-plus多表关联查询功能的实现代码,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2021-11-11
  • logback EvaluatorFilter日志过滤器源码解读

    logback EvaluatorFilter日志过滤器源码解读

    这篇文章主要为大家介绍了logback EvaluatorFilter日志过滤器源码解读,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-11-11
  • Java多态向上转型和向下转型及优点总结

    Java多态向上转型和向下转型及优点总结

    多态指不同对象对同一方法呈现不同行为,通过向上转型和重写实现,提升代码灵活性与可扩展性,降低复杂度,这篇文章主要介绍了Java多态向上转型和向下转型及优点的相关资料,需要的朋友可以参考下
    2025-06-06
  • Java读写Excel实例分享

    Java读写Excel实例分享

    本文主要分享了Java读写Excel的实例代码。具有一定的参考价值,下面跟着小编一起来看下吧
    2017-01-01
  • Java利用Hutool-Script封装JS脚本执行

    Java利用Hutool-Script封装JS脚本执行

    在 Java 开发中,有时需要动态执行脚本代码,比如 JavaScript 脚本,来实现一些灵活的业务逻辑,下面我们就来看看如何利用Hutool-Script模块对Java的脚本执行功能进行封装吧
    2025-02-02
  • Spring中使用@Value注解注入属性文件中的值详解

    Spring中使用@Value注解注入属性文件中的值详解

    这篇文章主要介绍了Spring中使用@Value注解注入属性文件中的值详解,通过Spring的@Value注解可以将xml中关联的属性文件中的值注入变量中,这样就不需要通过创建Properties然后根据属性文件读取属性值了,需要的朋友可以参考下
    2023-12-12
  • eclipse 安装lombok插件

    eclipse 安装lombok插件

    这篇文章主要介绍了eclipse 安装lombok插件的详细步骤,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2018-07-07
  • Java中的阻塞队列BlockingQueue使用详解

    Java中的阻塞队列BlockingQueue使用详解

    这篇文章主要介绍了Java中的阻塞队列BlockingQueue使用详解,阻塞队列是一种线程安全的数据结构,用于在多线程环境下进行数据交换,它提供了一种阻塞的机制,当队列为空时,消费者线程将被阻塞,直到队列中有数据可供消费,需要的朋友可以参考下
    2023-10-10

最新评论