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()));

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/79474.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Windows 10系统中找回MySQL 8的root密码

以下是 在Windows 10系统中找回MySQL 8的root密码 的详细步骤&#xff1a; 步骤1&#xff1a;停止MySQL服务 按 Win R 输入 services.msc&#xff0c;打开「服务」管理器。找到 MySQL80&#xff08;或其他自定义服务名&#xff09;&#xff0c;右键选择 停止。 步骤2&#xf…

【计网】互联网的组成

回顾&#xff1a; 互联网(Internet)&#xff1a;它是一个专有名词&#xff0c;是一个特定的互连网&#xff0c;它是指当下全球最大的、最开放的、由众多网络相互连接而形成的特定的的互连网&#xff0c;采用TCP/IP协议族作为通信规则。 一、互联网的组成部分 从互联网的工作方…

【vue3】黑马程序员前端Vue3小兔鲜电商项目【八】

黑马程序员前端Vue3小兔鲜电商项目【八】登录页面 登录页面的主要功能就是表单校验和登录登出业务。 账号密码 accountpasswordcdshi0080123456cdshi0081123456cdshi0082123456cdshi0083123456cdshi0084123456cdshi0085123456cdshi0086123456cdshi0087123456cdshi0088123456 …

C++学习:六个月从基础到就业——C++11/14:右值引用与移动语义

C学习&#xff1a;六个月从基础到就业——C11/14&#xff1a;右值引用与移动语义 本文是我C学习之旅系列的第三十九篇技术文章&#xff0c;也是第三阶段"现代C特性"的第一篇&#xff0c;主要介绍C11/14中引入的右值引用和移动语义。查看完整系列目录了解更多内容。 引…

基于Qlearning强化学习的电梯群控系统高效调度策略matlab仿真

目录 1.算法仿真效果 2.算法涉及理论知识概要 2.1 Q-learning强化学习原理 2.2 基于Q-learning的电梯群控系统建模 3.MATLAB核心程序 4.完整算法代码文件获得 1.算法仿真效果 matlab2022a仿真结果如下&#xff08;完整代码运行后无水印&#xff09;&#xff1a; 仿真操作…

31.软件时序控制方式抗干扰

软件时序控制方式扛干扰 1. 软件时序控制抗干扰的时间逻辑2. 应用案例 1. 软件时序控制抗干扰的时间逻辑 &#xff08;1&#xff09;将受软件控制的功能或软件检测到的状态一一罗列&#xff1b; &#xff08;2&#xff09;将其中的潜在干扰和敏感信号分开&#xff1b; &#x…

Ubuntu环境下使用uWSGI服务器【以flask应用部署为例】

0、前置内容说明 首先要知道WSGI是什么&#xff0c;关于WSGI服务器的介绍看这篇&#xff1a;WSGI&#xff08;Web Server Gateway Interface&#xff09;服务器 由于从Python 3.11开始限制了在系统级 Python 环境中使用 pip 安装第三方包&#xff0c;以避免与系统包管理器&am…

d3_v7绘制折线图

<!DOCTYPE html> <html><head><meta charsetutf-8><title>需求</title><script src"https://d3js.org/d3.v7.min.js"></script><style>* {margin: 0;padding: 0;}html, body {width: 100%;height: 100%;displ…

Hotspot分析(1):单细胞转录组识别信息基因(和基因模块)

这一期我们介绍一个常见的&#xff0c;高分文章引用很高的一个单细胞转录组分析工具Hotspot&#xff0c;它可针对单细胞转录组数据识别有意义基因或者基因module&#xff0c;类似于聚类模块。所谓的”informative "的基因是那些在给定度量中相邻的细胞之间以相似的方式表达…

爬虫准备前工作

1.Pycham的下载 网址&#xff1a;PyCharm: The only Python IDE you need 2.Python的下载 网址&#xff1a;python.org&#xff08;python3.9版本之后都可以&#xff09; 3.node.js的下载 网址&#xff1a;Node.js — 在任何地方运行 JavaScript&#xff08;版本使用18就可…

基于Springboot旅游网站系统【附源码】

基于Springboot旅游网站系统 效果如下&#xff1a; 系统登陆页面 系统主页面 景点信息推荐页面 路线详情页面 景点详情页面 确认下单页面 景点信息管理页面 旅游路线管理页面 研究背景 随着互联网技术普及与在线旅游消费习惯的深化&#xff0c;传统旅游服务模式面临效率低、…

利用KMP找出模式串在目标串中所有匹配位置的起始下标

问题关键&#xff1a;完成首次匹配之后需要继续进行模式匹配。 到这一步后&#xff0c;我们不能直接将j 0然后开始下一轮匹配&#xff0c;因为已经匹配过的部分&#xff08;蓝色部分&#xff09;中仍然可能存在与模式串重叠的子串&#xff1a; 解决办法&#xff1a; 找到蓝…

RR(Repeatable Read)级别如何防止幻读

在 MySQL 数据库事务隔离级别中&#xff0c;RR&#xff08;可重复读&#xff09; 通过 MVCC&#xff08;多版本并发控制&#xff09; 和 锁机制 的组合策略来避免幻读问题。 一、MVCC机制&#xff1a;快照读与版本控制 快照读&#xff08;Snapshot Read&#xff09; 每个事务启…

Android运行时ART加载类和方法的过程分析

目录 一,概述 二,ART运行时的入口 一,概述 既然ART运行时执行的都是翻译DEX字节码后得到的本地机器指令了&#xff0c;为什么还需要在OAT文件中包含DEX文件&#xff0c;并且将它加载到内存去呢&#xff1f;这是因为ART运行时提供了Java虚拟机接口&#xff0c;而要实现Java虚…

Javase 基础加强 —— 02 泛型

本系列为笔者学习Javase的课堂笔记&#xff0c;视频资源为B站黑马程序员出品的《黑马程序员JavaAI智能辅助编程全套视频教程&#xff0c;java零基础入门到大牛一套通关》&#xff0c;章节分布参考视频教程&#xff0c;为同样学习Javase系列课程的同学们提供参考。 01 认识泛型…

Oracle VirtualBox 在 macOS 上的详细安装步骤

Oracle VirtualBox 在 macOS 上的详细安装步骤 一、准备工作1. 系统要求2. 下载安装包二、安装 VirtualBox1. 挂载安装镜像2. 运行安装程序3. 处理安全限制(仅限首次安装)三、安装扩展包(增强功能)四、配置第一个虚拟机1. 创建新虚拟机2. 分配内存3. 创建虚拟硬盘4. 加载系…

RAGFlow 接入企业微信应用实现原理剖析与最佳实践

背景 近期有医美行业客户咨询我们智能客服产品&#xff0c;期望将自己企业的产品、服务以及报价信息以企微应用的方式给到客户进行体验互动&#xff0c;提升企业运营效率。关于企业微信对接&#xff0c;我们分享下最佳实践&#xff0c;抛砖引玉。效果图如下&#xff1a; 这里也…

【心海资源】子比主题新增注册与会员用户展示功能模块及实现方法

内容改写&#xff1a; 本次分享的是子比主题顶部展示注册用户与会员信息的功能模块及其实现方式。 你可以通过两种方式启用该功能&#xff1a; 直接在后台进入“外观 → 小工具”启用该展示模块&#xff0c;操作简便&#xff1b;也可将提供的代码覆盖至子比主题目录中&#…

CSDN积分详解(介绍、获取、用途)

&#x1f91f;致敬读者 &#x1f7e9;感谢阅读&#x1f7e6;笑口常开&#x1f7ea;生日快乐⬛早点睡觉 &#x1f4d8;博主相关 &#x1f7e7;博主信息&#x1f7e8;博客首页&#x1f7eb;专栏推荐&#x1f7e5;活动信息 文章目录 积分**一、积分类型及用途****二、积分获取途…

【iview】es6变量结构赋值(对象赋值)

变量的解构赋值 以iview的src/index.js中Vue.prototype.$IVIEW改造为例练习下怎么使用变量的解构赋值 原来的写法&#xff1a; const install function(Vue, opts {}) {if (install.installed) return;locale.use(opts.locale);locale.i18n(opts.i18n);Object.keys(iview).fo…