【Mybatis-Plus】根据自定义注解实现自动加解密

news2024/10/6 16:20:25

背景

我们把数据存到数据库的时候,有些敏感字段是需要加密的,从数据库查出来再进行解密。如果存在多张表或者多个地方需要对部分字段进行加解密操作,每个地方都手写一次加解密的动作,显然不是最好的选择。如果我们使用的是Mybatis框架,那就跟着一起探索下如何使用框架的拦截器功能实现自动加解密吧。

定义一个自定义注解

我们需要一个注解,只要实体类的属性加上这个注解,那么就对这个属性进行自动加解密。我们把这个注解定义灵活一点,不仅可以放在属性上,还可以放到类上,如果在类上使用这个注解,代表这个类的所有属性都进行自动加密。

/**
 * 加密字段
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.TYPE})
public @interface EncryptField {

}

定义实体类

package com.wen3.demo.mybatisplus.po;

import com.baomidou.mybatisplus.annotation.*;
import com.wen3.demo.mybatisplus.encrypt.annotation.EncryptField;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;

@EncryptField
@Getter
@Setter
@Accessors(chain = true)
@KeySequence(value = "t_user_user_id_seq", dbType = DbType.POSTGRE_SQL)
@TableName("t_USER")
public class UserPo {

    /**
     * 用户id
     */
    @TableId(value = "USER_ID", type = IdType.INPUT)
    private Long userId;

    /**
     * 用户姓名
     */
    @TableField("USER_NAME")
    private String userName;

    /**
     * 用户性别
     */
    @TableField("USER_SEX")
    private String userSex;

    /**
     * 用户邮箱
     */
    @EncryptField
    @TableField("USER_EMAIL")
    private String userEmail;

    /**
     * 用户账号
     */
    @TableField("USER_ACCOUNT")
    private String userAccount;

    /**
     * 用户地址
     */
    @TableField("USER_ADDRESS")
    private String userAddress;

    /**
     * 用户密码
     */
    @TableField("USER_PASSWORD")
    private String userPassword;

    /**
     * 用户城市
     */
    @TableField("USER_CITY")
    private String userCity;

    /**
     * 用户状态
     */
    @TableField("USER_STATUS")
    private String userStatus;

    /**
     * 用户区县
     */
    @TableField("USER_SEAT")
    private String userSeat;
}

拦截器

Mybatis-Plus有个拦截器接口com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor,但发现这个接口有一些不足

  • 必须构建一个MybatisPlusInterceptor这样的Bean
  • 并调用这个BeanaddInnerInterceptor方法,把所有的InnerInterceptor加入进去,才能生效
  • InnerInterceptor只有before拦截,缺省after拦截。加密可以在before里面完成,但解密需要在after里面完成,所以这个InnerInterceptor不能满足我们的要求

所以继续研究源码,发现Mybatis有个org.apache.ibatis.plugin.Interceptor接口,这个接口能满足我对自动加解密的所有诉求

  • 首先,实现Interceptor接口,只要注册成为Spring容器的Bean,拦截器就能生效
  • 可以更加灵活的在beforeafter之间插入自己的逻辑

加密拦截器

创建名为EncryptInterceptor的加密拦截器,对update操作进行拦截,对带@EncryptField注解的字段进行加密处理,无论是save方法还是saveBatch方法都会被成功拦截到。

package com.wen3.demo.mybatisplus.encrypt.interceptor;

import com.wen3.demo.mybatisplus.encrypt.annotation.EncryptField;
import com.wen3.demo.mybatisplus.encrypt.util.FieldEncryptUtil;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Objects;

/**
 * 对update操作进行拦截,对{@link EncryptField}字段进行加密处理;
 * 无论是save方法还是saveBatch方法都会被成功拦截;
 */
@Slf4j
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
@Component
public class EncryptInterceptor implements Interceptor {

    private static final String METHOD = "update";

    @Setter(onMethod_ = {@Autowired})
    private FieldEncryptUtil fieldEncryptUtil;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if(!StringUtils.equals(METHOD, invocation.getMethod().getName())) {
            return invocation.proceed();
        }

        // 根据update拦截规则,第0个参数一定是MappedStatement,第1个参数是需要进行判断的参数
        Object param = invocation.getArgs()[1];
        if(Objects.isNull(param)) {
            return invocation.proceed();
        }

        // 加密处理
        fieldEncryptUtil.encrypt(param);

        return invocation.proceed();
    }
}

解密拦截器

创建名为DecryptInterceptor的加密拦截器,对query操作进行拦截,对带@EncryptField注解的字段进行解密处理,无论是返回单个对象,还是对象的集合,都会被拦截到。

package com.wen3.demo.mybatisplus.encrypt.interceptor;

import cn.hutool.core.util.ClassUtil;
import com.wen3.demo.mybatisplus.encrypt.annotation.EncryptField;
import com.wen3.demo.mybatisplus.encrypt.util.FieldEncryptUtil;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.sql.Statement;
import java.util.Collection;

/**
 * 对query操作进行拦截,对{@link EncryptField}字段进行解密处理;
 */
@Slf4j
@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = Statement.class)
})
@Component
public class DecryptInterceptor implements Interceptor {

    private static final String METHOD = "query";

    @Setter(onMethod_ = {@Autowired})
    private FieldEncryptUtil fieldEncryptUtil;

    @SuppressWarnings("rawtypes")
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object result = invocation.proceed();

        // 解密处理
        // 经过测试发现,无论是返回单个对象还是集合,result都是ArrayList类型
        if(ClassUtil.isAssignable(Collection.class, result.getClass())) {
            fieldEncryptUtil.decrypt((Collection) result);
        } else {
            fieldEncryptUtil.decrypt(result);
        }

        return result;
    }
}

加解密工具类

由于加密和解密绝大部分的逻辑是相似的,不同的地方在于

  • 加密需要通过反射处理的对象,是在SQL执行前,是Invocation对象的参数列表中下标为1的参数;而解决需要通过反射处理的对象,是在SQL执行后,对执行结果对象进行解密处理。
  • 一个是获取到字段值进行加密,一个是获取到字段值进行解密

于是把加解密逻辑抽象成一个工具类,把差异的部分做为参数传入

package com.wen3.demo.mybatisplus.encrypt.util;

import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.ReflectUtil;
import com.wen3.demo.mybatisplus.encrypt.annotation.EncryptField;
import com.wen3.demo.mybatisplus.encrypt.service.FieldEncryptService;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.List;
import java.util.Objects;

/**
 * 加解密工具类
 */
@Slf4j
@Component
public class FieldEncryptUtil {

    @Setter(onMethod_ = {@Autowired})
    private FieldEncryptService fieldEncryptService;

    /**对EncryptField注解进行加密处理*/
    public void encrypt(Object obj) {
        if(ClassUtil.isPrimitiveWrapper(obj.getClass())) {
            return;
        }
        encryptOrDecrypt(obj, true);
    }

    /**对EncryptField注解进行解密处理*/
    public void decrypt(Object obj) {
        encryptOrDecrypt(obj, false);
    }

    /**对EncryptField注解进行解密处理*/
    public void decrypt(Collection list) {
        if(CollectionUtils.isEmpty(list)) {
            return;
        }
        list.forEach(this::decrypt);
    }

    /**对EncryptField注解进行加解密处理*/
    private void encryptOrDecrypt(Object obj, boolean encrypt) {
        // 根据update拦截规则,第0个参数一定是MappedStatement,第1个参数是需要进行判断的参数
        if(Objects.isNull(obj)) {
            return;
        }

        // 获取所有带加密注解的字段
        List<Field> encryptFields = null;
        // 判断类上面是否有加密注解
        EncryptField encryptField = AnnotationUtils.findAnnotation(obj.getClass(), EncryptField.class);
        if(Objects.nonNull(encryptField)) {
            // 如果类上有加密注解,则所有字段都需要加密
            encryptFields = FieldUtils.getAllFieldsList(obj.getClass());
        } else {
            encryptFields = FieldUtils.getFieldsListWithAnnotation(obj.getClass(), EncryptField.class);
        }

        // 没有字段需要加密,则跳过
        if(CollectionUtils.isEmpty(encryptFields)) {
            return;
        }

        encryptFields.forEach(f->{
            // 只支持String类型的加密
            if(!ClassUtil.isAssignable(String.class, f.getType())) {
                return;
            }

            String oldValue = (String) ReflectUtil.getFieldValue(obj, f);
            if(StringUtils.isBlank(oldValue)) {
                return;
            }

            String logText = null, newValue = null;
            if(encrypt) {
                logText = "encrypt";
                newValue = fieldEncryptService.encrypt(oldValue);
            } else {
                logText = "decrypt";
                newValue = fieldEncryptService.decrypt(oldValue);
            }

            log.info("{} success[{}=>{}]. before:{}, after:{}", logText, f.getDeclaringClass().getName(), f.getName(), oldValue, newValue);
            ReflectUtil.setFieldValue(obj, f, newValue);
        });
    }
}

加解密算法

Mybatis-Plus自带了一个AES加解密算法的工具,我们只需要提供一个加密key,然后就可以完成一个加解密的业务处理了。

  • 先定义一个加解密接口
package com.wen3.demo.mybatisplus.encrypt.service;

/**
 * 数据加解密接口
 */
public interface FieldEncryptService {

    /**对数据进行加密*/
    String encrypt(String value);

    /**对数据进行解密*/
    String decrypt(String value);

    /**判断数据是否忆加密*/
    default boolean isEncrypt(String value) {
        return false;
    }
}
  • 然后实现一个默认的加解密实现类
package com.wen3.demo.mybatisplus.encrypt.service.impl;

import cn.hutool.core.util.ClassUtil;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.AES;
import com.wen3.demo.mybatisplus.encrypt.service.FieldEncryptService;
import org.springframework.stereotype.Component;

import javax.crypto.IllegalBlockSizeException;

/**
 * 使用Mybatis-Plus自带的AES加解密
 */
@Component
public class DefaultFieldEncryptService implements FieldEncryptService {

    private static final String ENCRYPT_KEY = "abcdefghijklmnop";

    @Override
    public String encrypt(String value) {
        if(isEncrypt(value)) {
            return value;
        }
        return AES.encrypt(value, ENCRYPT_KEY);
    }

    @Override
    public String decrypt(String value) {
        return AES.decrypt(value, ENCRYPT_KEY);
    }

    @Override
    public boolean isEncrypt(String value) {
        // 判断是否已加密
        try {
            // 解密成功,说明已加密
            decrypt(value);
            return true;
        } catch (MybatisPlusException e) {
            if(ClassUtil.isAssignable(IllegalBlockSizeException.class, e.getCause().getClass())) {
                return false;
            }
            throw e;
        }
    }
}

自动加解密单元测试

package com.wen3.demo.mybatisplus.service;

import cn.hutool.core.util.RandomUtil;
import com.wen3.demo.mybatisplus.MybatisPlusSpringbootTestBase;
import com.wen3.demo.mybatisplus.encrypt.service.FieldEncryptService;
import com.wen3.demo.mybatisplus.po.UserPo;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;
import java.util.Map;

class UserServiceTest extends MybatisPlusSpringbootTestBase {

    @Resource
    private UserService userService;
    @Resource
    private FieldEncryptService fieldEncryptService;

    @Test
    void save() {
        UserPo userPo = new UserPo();
        String originalValue = RandomStringUtils.randomAlphabetic(16);
        String encryptValue = fieldEncryptService.encrypt(originalValue);
        userPo.setUserEmail(originalValue);
        userPo.setUserName(RandomStringUtils.randomAlphabetic(16));
        boolean testResult = userService.save(userPo);
        assertTrue(testResult);
        assertNotEquals(originalValue, userPo.getUserEmail());
        assertEquals(encryptValue, userPo.getUserEmail());

        // 测试解密: 返回单个对象
        UserPo userPoQuery = userService.getById(userPo.getUserId());
        assertEquals(originalValue, userPoQuery.getUserEmail());
        // 测试解密: 返回List
        List<UserPo> userPoList = userService.listByEmail(encryptValue);
        assertEquals(originalValue, userPoList.get(0).getUserEmail());

        // 测试saveBatch方法也会被拦截加密
        userPo.setUserId(null);
        testResult = userService.save(Collections.singletonList(userPo));
        assertTrue(testResult);
        assertNotEquals(originalValue, userPo.getUserEmail());
        assertEquals(encryptValue, userPo.getUserEmail());
    }
}

单元测试运行截图

在这里插入图片描述

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

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

相关文章

基于WPF技术的换热站智能监控系统13--控制设备开关

1、本节目的 本次工作量相对有点大&#xff0c;有点难度&#xff0c;需要熟悉MVVM模式&#xff0c;特别是属性绑定和命令驱动&#xff0c;目标是点击水泵开关&#xff0c;让风扇转动或停止&#xff0c;风扇连接的管道液体流动或静止。 &#xff0c;具体对应关系是&#xff1a;…

单向散列函数解析

目录 1. 概述 2. 单向散列函数的性质 2.1 根据任意长度的消息计算出固定长度的散列值 2.2 能够快速计算出散列值 2.3 消息不同散列值也不同 2.4 具备单向性 3. 单向散列函数的算法 3.1 MD5 3.2 SHA序列 3.3 SM3 1. 概述 针对计算机所处理的消息&#xff0c;有时候我们…

【设计模式深度剖析】【9】【行为型】【访问者模式】| 以博物馆的导览员为例加深理解

&#x1f448;️上一篇:备忘录模式 设计模式-专栏&#x1f448;️ 文章目录 访问者模式定义英文原话直译如何理解呢&#xff1f; 访问者模式的角色类图代码示例 访问者模式的应用优点缺点使用场景 示例解析:博物馆的导览员代码示例 访问者模式 访问者模式&#xff08;Visito…

计算机毕业设计hadoop+spark+hive知识图谱酒店推荐系统 酒店数据分析可视化大屏 酒店爬虫 高德地图API 酒店预测系统 大数据毕业设计

流程&#xff1a; 1.Python爬取去哪儿网全站旅游数据约10万&#xff0c;存入mysql; 2.使用pandasnumpy/hadoopmapreduce对mysql中旅游数据进行数据清洗&#xff0c;使用高德API计算地理信息&#xff0c;最终转为.csv文件上传hdfs; 3.hive建库建表导入.csv文件作为数据集&#x…

2024年汉字小达人活动还有4个多月开赛:来做18道历年选择题备考吧

结合最近几年的活动安排&#xff0c;预计2024年第11届汉字小达人比赛还有4个多月就启动&#xff0c;那么孩子们如何利用这段时间有条不紊地准备汉字小达人比赛呢&#xff1f; 我的建议是充分利用即将到来的暑假&#xff1a;①把小学1-5年级的语文课本上的知识点熟悉&#xff0…

Windows11和Ubuntu22双系统安装指南

一、需求描述 台式机电脑&#xff0c;已有Windows11操作系统&#xff0c;想要安装Ubuntu22系统&#xff08;版本任意&#xff09;。其中Windows安装在Nvme固态上&#xff0c;Ubuntu安装在Sata固态上&#xff0c;双盘双系统。开机时使用Grub控制进入哪个系统&#xff0c;效果图…

直接选择排序-C语言版本

前言 直接选择排序也是一个比较简单的排序&#xff0c;所以这里放在第二个进行讲解&#xff0c;这里和冒泡排序是有一点相似。直接选择排序和冒泡排序一样&#xff0c;也是具备一定的教学意义&#xff0c;但是没有什么实际操作的意义&#xff0c;因为直接选择排序的时间复杂度比…

云原生 Docker Swarm 使用详解

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~~ &#x1f4a5;&#x1f4a5;个人主页&#xff1a;奋斗的小羊 &#x1f4a5;&#x1f4a5;所属专栏&#xff1a;C语言 &#x1f680;本系列文章为个人学习…

用Copilot画漫画,Luma AI生成视频:解锁创意新玩法

近年来&#xff0c;随着人工智能技术的不断发展&#xff0c;各种创意工具也层出不穷。今天&#xff0c;我们就来介绍一种全新的创作方式&#xff1a;使用Copilot画漫画&#xff0c;再将漫画放入Luma AI生成视频。 Copilot&#xff1a;你的AI绘画助手 Copilot是一款基于人工智…

Java | Leetcode Java题解之第147题对链表进行插入排序

题目&#xff1a; 题解&#xff1a; class Solution {public ListNode insertionSortList(ListNode head) {if (head null) {return head;}ListNode dummyHead new ListNode(0);dummyHead.next head;ListNode lastSorted head, curr head.next;while (curr ! null) {if (…

C++编程:vector容器的简单模拟实现

前言&#xff1a; 在C标准库&#xff08;STL&#xff09;中&#xff0c;vector容器是最常见使用的动态数组。它结合了链表与数组的优点&#xff0c;提供了灵活的大小调整与高效的随机访问。本文将简单的对vector容器进行介绍并且对vector容器简单的模拟实现。 一、vector的文…

Project ERROR: Unknown module(s) in QT: xlsx

Qt5下Qxlsx模块安装及使用_qt5xlsx-CSDN博客 主要参考上面这篇文章&#xff01; Perl的安装与配置_perl安装-CSDN博客 1.1 windows环境安装Perl_windows perl-CSDN博客 首先&#xff0c;需要安装Perl,我安装的是Windows版本的。 Download & Install Perl - ActiveStat…

C#使用Scoket实现服务器和客户端互发信息

20240616 By wdhuag 目录 前言&#xff1a; 参考&#xff1a; 一、服务器端&#xff1a; 1、服务器端口绑定&#xff1a; 2、服务器关闭&#xff1a; 二、客户端&#xff1a; 1、客户端连接&#xff1a; 2、客户端断开&#xff1a; 三、通讯&#xff1a; 1、接收信…

【后端】websocket学习笔记

文章目录 1. 消息推送常见方式1.1 轮询 VS 长轮询1.2 SSE&#xff08;server-sent event)服务器发送事件 2. websocket介绍2.1 介绍2.2 原理2.3 websoket API2.3.1 客户端【浏览器】API2.3.2 服务端API 3. 代码实现3.1 流程分析3.2 pom依赖3.3 配置类3.4 消息格式3.5 消息类 4.…

Qwen2大语言模型微调、导出、部署实践

上篇文章&#xff1a; Qwen1.5大语言模型微调实践_qwen1.5 7b微调-CSDN博客 我们介绍了Qwen1.5 大语言模型使用LLaMA-Factory 来微调&#xff0c;这篇文章我们介绍一下微调后模型的导出、部署。 一、模型导出 在webui 界面训练好模型之后点击“Export”选项卡&#xff0c;然…

Golang | Leetcode Golang题解之第155题最小栈

题目&#xff1a; 题解&#xff1a; type MinStack struct {stack []intminStack []int }func Constructor() MinStack {return MinStack{stack: []int{},minStack: []int{math.MaxInt64},} }func (this *MinStack) Push(x int) {this.stack append(this.stack, x)top : thi…

工程设计问题---压缩弹簧设计

参考文献&#xff1a; [1] 吴擎, 徐惟罡, 张春江. 基于师生交流机制的改进类电磁机制算法[J]. 计算机集成制造系统, 2020, 26(4): 1033-1042.

数电逻辑门电路分析和Digital仿真

文章目录 1. 逻辑门电路 2. 非门&#xff08;NOT Gate&#xff09; 3. 与门&#xff08;AND Gate&#xff09; 4. 或门&#xff08;OR Gate&#xff09; 5. 与非门&#xff08;NAND Gate&#xff09; 6. 或非门&#xff08;NOR Gate&#xff09; 7. 异或门&#xff08;XO…

VMware 桥接网络突然无法上网

VMware 桥接网络突然无法上网 0. 问题1. 解决方法 0. 问题 昨天&#xff0c;VMware 桥接网络正常使用&#xff0c;今天突然无法上网。 1. 解决方法 打开VMware的虚拟网络编辑器&#xff0c;将桥接模式的网络从“自动”改成你要使用的网卡&#xff0c;问题解决。 完成&#…

【CT】LeetCode手撕—88. 合并两个有序数组

目录 题目1- 思路2- 实现⭐88. 合并两个有序数组——题解思路 2- ACM实现 题目 原题连接&#xff1a;88. 合并两个有序数组 1- 思路 模式识别 模式1&#xff1a;两个有序数组合并 ——> 双指针模式2&#xff1a;返回结果填充到 nums1[mn] ——> 需要开辟新的数组空间 …