文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Java操作MyBatis-Plus通过自定义拦截器对mysql字段以注解形式实现自动加解密

2023-09-06 07:16

关注

一.需求背景

跟大学室友闲谈时,了解到他公司正在做项目内对数据库敏感字段实现自动加解密的需求,使用的技术是Springboot,Mybatis-Plus,MySql等技术栈,加密算法是用的AES,密钥是放在华为云,这里实现一个阉割版的demo,仅供有兴趣的同学进行参考。

二.前置条件

首先我自己在日常搭了一个普通的springboot项目,目前还没有前台,所以就在浏览器请求tomcat模拟接口。

其次,这里的是实现主要是应用了Mybatis的拦截器,AES算法,mysql等技术栈,需要了解一下相关背景。

1.AES算法

AES的全称是Advanced Encryption Standard,意思是高级加密标准。它的出现主要是为了取代DES加密算法的,因为我们都知道DES算法的密钥长度是56Bit,因此算法的理论安全强度是2的56次方。但二十世纪中后期正是计算机飞速发展的阶段,元器件制造工艺的进步使得计算机的处理能力越来越强,虽然出现了3DES的加密方法,但由于它的加密时间是DES算法的3倍多,64Bit的分组大小相对较小,所以还是不能满足人们对安全性的要求。于是1997年1月2号,美国国家标准技术研究所宣布希望征集高级加密标准,用以取代DES。AES也得到了全世界很多密码工作者的响应,先后有很多人提交了自己设计的算法。最终有5个候选算法进入最后一轮:Rijndael,Serpent,Twofish,RC6和MARS。最终经过安全性分析、软硬件性能评估等严格的步骤,Rijndael算法获胜。

在密码标准征集中,所有AES候选提交方案都必须满足以下标准:

2.Mybatis拦截器

拦截器(Interceptor)在 Mybatis 中被当做插件(plugin)对待,官方文档提供了 Executor(拦截执行器的方法),ParameterHandler(拦截参数的处理),ResultSetHandler(拦截结果集的处理),StatementHandler(拦截Sql语法构建的处理) 共4种,并且提示“这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码”。

拦截器的使用场景主要是更新数据库的通用字段,分库分表,加解密等的处理。

1.1 MyBatis自定义拦截器

1.2 在MyBatis中可被拦截的类型有四种(按照拦截顺序)

先执行每个插件的plugin方法,若是@Intercepts注解标明需要拦截该对象,那么生成类型对象的代理对象。(即使该插件需要拦截该类型对象,但是依旧会执行下一个插件的plugin方法)。知道执行完毕所有的plugin方法。在执行每个Intercept方法。

1.3 拦截器注解的作用:

自定义拦截器必须使用MyBatis提供的注解来声明我们要拦截的类型对象。

Mybatis插件都要有Intercepts注解来指定要拦截哪个对象哪个方法。我们知道,Plugin.wrap方法会返回四大接口对象的代理对象,会拦截所有的方法。在代理对象执行对应方法的时候,会调用InvocationHandler处理器的invoke方法。

1.4 拦截器注解的规则:

具体规则如下:

@Intercepts({    @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),    @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),    @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})})复制代码

1.5. 拦截器可拦截的方法

拦截的类拦截的方法
Executorupdate, query, flushStatements, commit, rollback,getTransaction, close, isClosed
ParameterHandlergetParameterObject, setParameters
StatementHandlerprepare, parameterize, batch, update, query
ResultSetHandlerhandleResultSets, handleOutputParameters

Executor 提供的方法中,update 包含了 新增,修改和删除类型,无法直接区分,需要借助 MappedStatement 类的属性 SqlCommandType 来进行判断,该类包含了所有的操作类型

public enum SqlCommandType {  UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;}

毕竟新增和修改的场景,有些参数是有区别的,比如创建时间和更新时间,update 时是无需兼顾创建时间字段的。

3.mysql数据库创建表

create table user(    id          bigint unsigned auto_increment comment '主键'        primary key,    name        varchar(20)        not null comment '姓名',    balance     int         default 0                          not null comment '账户余额',    password    varchar(50) not null comment '密码',    create_time datetime    default CURRENT_TIMESTAMP          not null comment '创建时间',    update_time datetime    default CURRENT_TIMESTAMP          null on update CURRENT_TIMESTAMP comment '更新时间')    comment '用户表';

这里的password就是我们要加解密的字段,存储时要进行加密,获取查询后进行解密。

4.Maven依赖

                com.baomidoumybatis-plus-boot-starter3.4.0        com.baomidoumybatis-plus-boot-starter3.4.0mysqlmysql-connector-java8.0.17

三.代码实现

实现思路就是首先通过注解,能确定需要加解密的表和字段,然后通过自定义注解进行标注,通过自定义拦截器对字段进行加解密操作。

1.AES加解密工具类

import javax.crypto.Cipher;import javax.crypto.spec.SecretKeySpec;import java.util.Base64;public class AESUtils {    private static final String ALGORITHM = "AES";    private static final String SECRET_KEY = "aes-key-lima1995"; // 密钥        public static String encrypt(String value) throws Exception {        SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), ALGORITHM);        Cipher cipher = Cipher.getInstance(ALGORITHM);        cipher.init(Cipher.ENCRYPT_MODE, keySpec);        byte[] encrypted = cipher.doFinal(value.getBytes());        return Base64.getEncoder().encodeToString(encrypted);    }        public static String decrypt(String value) throws Exception {        SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), ALGORITHM);        Cipher cipher = Cipher.getInstance(ALGORITHM);        cipher.init(Cipher.DECRYPT_MODE, keySpec);        byte[] decoded = Base64.getDecoder().decode(value);        byte[] decrypted = cipher.doFinal(decoded);        return new String(decrypted);    }}

2.自定义注解

import java.lang.annotation.ElementType;import java.lang.annotation.Inherited;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Inherited@Target({ ElementType.TYPE })@Retention(RetentionPolicy.RUNTIME)public @interface SensitiveData {}
import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.FIELD)public @interface Encrypted {}

3.加密拦截器

import lombok.extern.slf4j.Slf4j;import org.apache.ibatis.executor.parameter.ParameterHandler;import org.apache.ibatis.plugin.*;import org.springframework.core.annotation.AnnotationUtils;import org.springframework.stereotype.Component;import java.lang.reflect.Field;import java.sql.PreparedStatement;import java.util.Objects;@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();            // 获取参数对像,即 mapper 中 paramsType 的实例            Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");            parameterField.setAccessible(true);            //取出实例            Object parameterObject = parameterField.get(parameterHandler);            if (parameterObject != null) {                Class parameterObjectClass = parameterObject.getClass();                //校验该实例的类是否被@SensitiveData所注解                SensitiveData sensitiveData = AnnotationUtils.findAnnotation(parameterObjectClass, SensitiveData.class);                if (Objects.nonNull(sensitiveData)) {                    //取出当前当前类所有字段,传入加密方法                    Field[] declaredFields = parameterObjectClass.getDeclaredFields();                    encrypt(declaredFields, parameterObject);                }            }            return invocation.proceed();        } catch (Exception e) {            log.error("加密失败", e);        }        return invocation.proceed();    }        @Override    public Object plugin(Object o) {        return Plugin.wrap(o, this);    }    public  T encrypt(Field[] declaredFields, T paramsObject) throws Exception {        for (Field field : declaredFields) {            //取出所有被EncryptDecryptField注解的字段            Encrypted sensitiveField = field.getAnnotation(Encrypted.class);            if (!Objects.isNull(sensitiveField)) {                field.setAccessible(true);                Object object = field.get(paramsObject);                //暂时只实现String类型的加密                if (object instanceof String) {                    String value = (String) object;                    //加密  这里我使用自定义的AES加密工具                    field.set(paramsObject, AESUtils.encrypt(value));                }            }        }        return paramsObject;    }}

4.解密拦截器

import org.apache.ibatis.executor.resultset.ResultSetHandler;import org.apache.ibatis.plugin.*;import org.springframework.core.annotation.AnnotationUtils;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.Objects;@Intercepts({        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})})@Slf4j@Componentpublic class DecryptInterceptor implements Interceptor {    @Override    public Object intercept(Invocation invocation) throws Throwable {        Object resultObject = invocation.proceed();        try {            if (Objects.isNull(resultObject)) {                return null;            }            //基于selectList            if (resultObject instanceof ArrayList) {                ArrayList resultList = (ArrayList) resultObject;                if (!CollectionUtils.isEmpty(resultList) && needToDecrypt(resultList.get(0))) {                    for (Object result : resultList) {                        //逐一解密                        decrypt(result);                    }                }                //基于selectOne            } else {                if (needToDecrypt(resultObject)) {                    AESUtils.decrypt((String) resultObject);                }            }            return resultObject;        } catch (Exception e) {            log.error("解密失败", e);        }        return resultObject;    }    private boolean needToDecrypt(Object object) {        Class objectClass = object.getClass();        SensitiveData sensitiveData = AnnotationUtils.findAnnotation(objectClass, SensitiveData.class);        return Objects.nonNull(sensitiveData);    }    @Override    public Object plugin(Object o) {        return Plugin.wrap(o, this);    }    public  T decrypt(T result) throws Exception {        //取出resultType的类        Class resultClass = result.getClass();        Field[] declaredFields = resultClass.getDeclaredFields();        for (Field field : declaredFields) {            //取出所有被EncryptDecryptField注解的字段            Encrypted sensitiveField = field.getAnnotation(Encrypted.class);            if (!Objects.isNull(sensitiveField)) {                field.setAccessible(true);                Object object = field.get(result);                //只支持String的解密                if (object instanceof String) {                    String value = (String) object;                    //对注解的字段进行逐一解密                    field.set(result, AESUtils.decrypt(value));                }            }        }        return result;    }}

5.自定义controller、service、dao等

@RestControllerpublic class UserController {    @Autowired    UserService userService;    @RequestMapping("/create")    public String create(String name,String password) throws Exception {        Boolean result = userService.create(name,password);        if(result){            return "创建成功";        }        return "创建失败";    }    @RequestMapping("/query")    public User query(Long id){        return userService.query(id);    }}
@Servicepublic class UserServiceImpl implements UserService{    @Autowired    UserDao userDao;    @Override    public Boolean create(String name, String password) throws Exception {        return userDao.create(name,1000,password);    }    @Override    public User query(Long id) {        return userDao.query(id);    }}
@Componentpublic class UserDao {    @Resource    UserMapper userMapper;    public Boolean create(String name, Integer balance, String password) throws Exception {        User user =new User();        user.setName(name);        user.setBalance(balance);        user.setPassword(password);        int insert = userMapper.insert(user);        return insert > 0;    }    public User query(Long id) {        return userMapper.getUserById(id);    }}

四.结果演示

1.创建

这里使用postman模拟请求本地,查看数据库结果 

2.查询

业务代码中没有任何关于加解密的代码,但是在插入和查询时,已经自动进行了加解密操作。

以上就是全部内容,希望对你有所帮助。

参考资料:

https://juejin.cn/post/7027633293684113421

Mybatis-plus实现在DAO层面对敏感数据的加解密_@sensitivefield_一缕82年的清风的博客-CSDN博客

来源地址:https://blog.csdn.net/weixin_43029331/article/details/129383047

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     807人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     351人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     314人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     433人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     221人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-数据库
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯