mybatisplus配置拦截器实现保存加密,输出解密,模糊查询

news2025/7/17 0:21:02

前言:因公司需求需要把某些实体类的某些字段值进行加密保存,在查询时解密明文输出。现记录两种方式。

一、第一种方式:

(1)使用@TableField(typeHandler = TypeHandler.class)注解自带的字段类型处理器,写一个 handle 类继承 BaseTypeHandler。本身这个是用来处理字段类型转换的,如map转list之类的,这里也可以用作值加密。

注:使用这种方式有个前提,需要在@TableName注解上加上autoResultMap = true,才能使查询生效,否则只是新增修改生效进行拦截。没看源码猜想应该是先查整个实体有没有autoResultMap = true,再逐个进行字段拦截处理优化效率。

@Data
@TableName(value = "xxx",autoResultMap = true)
public class xxx implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(type = IdType.NONE, value = "id")
    private String id;

    @ApiModelProperty(value = "用户名")
    @TableField(value = "username",typeHandler = TypeControlHandler.class)
    private String username;

    @ApiModelProperty(value = "手机号")
    @TableField(value = "phone",typeHandler = TypeControlHandler.class)
    private String phone;
}

(2)定义加解密方式-自定义(这里配合使用了mysql的AES_ENCRYPTAES_DECRYPT函数加解密,主要是为了实现模糊查询,具体加解密方式可以自定义)

//需要导包:
//<dependency>
//	<groupId>commons-codec</groupId>
//	<artifactId>commons-codec</artifactId>
//</dependency>


import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

@Slf4j
public class DBAesUtils {
/** AES 加解密密钥,请勿擅自修改!!! */
    public static final String key = "唯一加解密密匙key-自定义";


    /**
     * AES 加密 使用AES-128-ECB加密模式
     * @param sSrc  需要加密的字段
     * @param sKey  16 位密钥
     * @return
     * @throws Exception
     */
    public static String Encrypt(String sSrc, String sKey) {
        try {
            if (sKey == null) {
                return null;
            }
            /** 判断Key是否为16位 */
            if (sKey.length() != 16) {
                return null;
            }
            byte[] raw = sKey.getBytes("utf-8");
            SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
            /** "算法/模式/补码方式" */
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
            byte[] encrypted = cipher.doFinal(sSrc.getBytes("utf-8"));
            /** 此处使用BASE64做转码功能,同时能起到2次加密的作用。 */
            return new Base64().encodeToString(encrypted);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public static String Encrypt(String sSrc) {
        return Encrypt(sSrc, key);
    }

    /**
     * AES 解密 使用AES-128-ECB加密模式
     * @param sSrc  需要解密的字段
     * @param sKey  16 位密钥
     * @return
     * @throws Exception
     */
    public static String Decrypt(String sSrc, String sKey) {
        try {
            // 判断Key是否正确
            if (sKey == null) {
                return null;
            }
            // 判断Key是否为16位
            if (sKey.length() != 16) {
                return null;
            }
            byte[] raw = sKey.getBytes("utf-8");
            SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            cipher.init(Cipher.DECRYPT_MODE, skeySpec);
            /** 先用base64解密 */
            byte[] encrypted1 = new Base64().decode(sSrc);
            try {
                byte[] original = cipher.doFinal(encrypted1);
                String originalString = new String(original,"utf-8");
                return originalString;
            } catch (Exception e) {
                System.out.println(e.toString());
                return null;
            }
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    public static String Decrypt(String sSrc) {
        return Decrypt(sSrc, key);
    }
  }

(3)定义具体拦截处理实现

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class TypeControlHandler extends BaseTypeHandler<String> {
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, DBAesUtils.Encrypt(parameter));
    }

    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return DBAesUtils.Decrypt(rs.getString(columnName));
    }

    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return DBAesUtils.Decrypt(rs.getString(columnIndex));
    }

    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return DBAesUtils.Decrypt(cs.getString(columnIndex));
    }

}

(4)最终实现结果(有个小问题就是如果加密前字符一样,加密出来密文就是一样的)
在这里插入图片描述
至于加密的字段查询并没有想到好的方法,目前想到的只能不使用LambdaQueryWrapper,使用QueryWrapper写死条件进行查询,这里配合了mysql的加解密函数,其他数据库需更改加解密方式才能使用。

wrapper.like("AES_DECRYPT(FROM_BASE64(字段名),'" + key + "')", 查询值);

第二种方式(其实实现原理就是第一种方式,只不过加了相对来说更灵活的扩展):

(1)定义两个注解

/**
 * 需要加解密的字段用这个注解
 * @author wangshaopeng@talkweb.com.cn
 * @Date 2023-05-31
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface EncryptedColumn {

}

/**
 * 需要加解密的实体类用这个注解
 * @author wangshaopeng@talkweb.com.cn
 * @Date 2023-05-31
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface EncryptedTable {

}

(2)实现InnerInterceptor进行拦截加密操作(使用第一种方法的DBAesUtils工具类进行加密)

import cn.hutool.core.util.ReflectUtil;
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import com.baomidou.mybatisplus.core.conditions.update.Update;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.springframework.core.annotation.AnnotationUtils;

import java.lang.reflect.Field;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@SuppressWarnings({"rawtypes"})
public class EncryptInterceptor extends JsqlParserSupport implements InnerInterceptor {
 /**
     * 变量占位符正则
     */
    private static final Pattern PARAM_PAIRS_RE = Pattern.compile("#\\{ew\\.paramNameValuePairs\\.(" + Constants.WRAPPER_PARAM + "\\d+)\\}");

    @Override
    public void beforeUpdate(Executor executor, MappedStatement mappedStatement, Object parameterObject) throws SQLException {
        if (Objects.isNull(parameterObject)) {
            return;
        }
        // 通过MybatisPlus自带API(save、insert等)新增数据库时
        if (!(parameterObject instanceof Map)) {
            if (needToDecrypt(parameterObject.getClass())) {
                encryptEntity(parameterObject);
            }
            return;
        }
        Map paramMap = (Map) parameterObject;
        Object param;
        // 通过MybatisPlus自带API(update、updateById等)修改数据库时
        if (paramMap.containsKey(Constants.ENTITY) && null != (param = paramMap.get(Constants.ENTITY))) {
            if (needToDecrypt(param.getClass())) {
                encryptEntity(param);
            }
            return;
        }
        // 通过在mapper.xml中自定义API修改数据库时
        if (paramMap.containsKey("entity") && null != (param = paramMap.get("entity"))) {
            if (needToDecrypt(param.getClass())) {
                encryptEntity(param);
            }
            return;
        }
        // 通过UpdateWrapper、LambdaUpdateWrapper修改数据库时
        if (paramMap.containsKey(Constants.WRAPPER) && null != (param = paramMap.get(Constants.WRAPPER))) {
            if (param instanceof Update && param instanceof AbstractWrapper) {
                Class<?> entityClass = mappedStatement.getParameterMap().getType();
                if (needToDecrypt(entityClass)) {
                    encryptWrapper(entityClass, param);
                }
            }
            return;
        }
    }

    /**
     * 校验该实例的类是否被@EncryptedTable所注解
     */
    private boolean needToDecrypt(Class<?> objectClass) {
        try {
            EncryptedTable sensitiveData = AnnotationUtils.findAnnotation(objectClass, EncryptedTable.class);
            return Objects.nonNull(sensitiveData);
        }catch (Exception ex){
            return  false;
        }

    }

    /**
     * 通过API(save、updateById等)修改数据库时
     *
     * @param parameter
     */
    private void encryptEntity(Object parameter) {
        //取出parameterType的类
        Class<?> resultClass = parameter.getClass();

        Field[] declaredFields =  ReflectUtil.getFields(resultClass);
        for (Field field : declaredFields) {
            //取出所有被EncryptedColumn注解的字段
            EncryptedColumn sensitiveField = field.getAnnotation(EncryptedColumn.class);
            if (!Objects.isNull(sensitiveField)) {
                field.setAccessible(true);
                Object object = null;
                try {
                    object = field.get(parameter);
                } catch (IllegalAccessException e) {
                    continue;
                }
                //只支持String的解密
                if (object instanceof String) {
                    String value = (String) object;
                    //对注解的字段进行逐一加密
                    try {
                        field.set(parameter, DBAesUtils.Encrypt(value));
                    } catch (IllegalAccessException e) {
                        continue;
                    }
                }
            }
        }
    }

    /**
     * 通过UpdateWrapper、LambdaUpdateWrapper修改数据库时
     *
     * @param entityClass
     * @param ewParam
     */
    private void encryptWrapper(Class<?> entityClass, Object ewParam) {
        AbstractWrapper updateWrapper = (AbstractWrapper) ewParam;
        String sqlSet = updateWrapper.getSqlSet();
        String[] elArr = sqlSet.split(",");
        Map<String, String> propMap = new HashMap<>(elArr.length);
        Arrays.stream(elArr).forEach(el -> {
            String[] elPart = el.split("=");
            propMap.put(elPart[0], elPart[1]);
        });

        //取出parameterType的类
        Field[] declaredFields = ReflectUtil.getFields(entityClass);
        for (Field field : declaredFields) {
            //取出所有被EncryptedColumn注解的字段
            EncryptedColumn sensitiveField = field.getAnnotation(EncryptedColumn.class);
            if (Objects.isNull(sensitiveField)) {
                continue;
            }
            String el = propMap.get(field.getName());
            Matcher matcher = PARAM_PAIRS_RE.matcher(el);
            if (matcher.matches()) {
                String valueKey = matcher.group(1);
                Object value = updateWrapper.getParamNameValuePairs().get(valueKey);
                updateWrapper.getParamNameValuePairs().put(valueKey, DBAesUtils.Encrypt(value.toString()));
            }
        }
    }
  }

(3)实现Interceptor进行拦截解密


import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.core.annotation.AnnotationUtils;
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})
})
public class DecryptInterceptor implements Interceptor {


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object resultObject = invocation.proceed();
        if (Objects.isNull(resultObject)) {
            return null;
        }
        if(resultObject instanceof IPage){
            IPage page = (IPage) resultObject;
            if(page!=null&& CollUtil.isNotEmpty(page.getRecords())){
                if (needToDecrypt(page.getRecords().get(0))) {
                    for (Object result : page.getRecords()) {
                        //逐一解密
                        decrypt(result);
                    }
                }
            }
        }
        else if (resultObject instanceof ArrayList) {
            //基于selectList
            ArrayList resultList = (ArrayList) resultObject;
            if (CollUtil.isNotEmpty(resultList) && needToDecrypt(resultList.get(0))) {
                for (Object result : resultList) {
                    //逐一解密
                    decrypt(result);
                }
            }
        } else if (needToDecrypt(resultObject)) {
            //基于selectOne
            decrypt(resultObject);
        }
        return resultObject;
    }

    /**
     * 校验该实例的类是否被@EncryptedTable所注解
     */
    private boolean needToDecrypt(Object object) {
        Class<?> objectClass = object.getClass();
        EncryptedTable sensitiveData = AnnotationUtils.findAnnotation(objectClass, EncryptedTable.class);
        return Objects.nonNull(sensitiveData);
    }

    @Override
    public Object plugin(Object o) {
        if(o instanceof ResultSetHandler) {
          return   Plugin.wrap(o, this);
        }else{
            return o;
        }
    }

    private <T> T decrypt(T result) throws Exception {
        //取出resultType的类
        Class<?> resultClass = result.getClass();
        Field[] declaredFields = ReflectUtil.getFields(resultClass);
        for (Field field : declaredFields) {
            //取出所有被EncryptedColumn注解的字段
            EncryptedColumn sensitiveField = field.getAnnotation(EncryptedColumn.class);
            if (!Objects.isNull(sensitiveField)) {
                field.setAccessible(true);
                Object object = field.get(result);
                //只支持String的解密
                if (object instanceof String) {
                    String value = (String) object;
                    //对注解的字段进行逐一解密
                    String results =  DBAesUtils.Decrypt(value);
                    if(StrUtil.isNotEmpty(results)){
                        field.set(result, results);
                    }else{
                        field.set(result, value);
                    }

                }
            }
        }
        return result;
    }
}

注:若是作为单独的starter进行封装,此拦截器需要定义在spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.xxx.xxx.xxx.xxx.DecryptInterceptor

(4)实体类加上注解(需要@EncryptedTable@EncryptedColumn配合使用)

@Data
@TableName(value = "xxx",autoResultMap = true)
@EncryptedTable
public class xxx implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(type = IdType.NONE, value = "id")
    private String id;

    @ApiModelProperty(value = "用户名")
    @TableField(value = "username")
    @EncryptedColumn
    private String username;

    @ApiModelProperty(value = "手机号")
    @TableField(value = "phone")
    @EncryptedColumn
    private String phone;
}

实现结果与第一种方式一样

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

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

相关文章

盲盒游戏开发方案

盲盒游戏是一种新兴的游戏类型&#xff0c;其核心机制是让玩家在购买时不知道会获得哪一种物品&#xff0c;从而产生一种探索的乐趣。那么&#xff0c;如何开发一个盲盒游戏呢&#xff1f;以下是一些步骤和要点。 一、确定游戏目标和规则 在开发盲盒游戏时&#xff0c;…

前端:nodejs多版本管理工具nvm

nvm是什么 nvm全英文也叫node.js version management&#xff0c;是一个nodejs的版本管理工具。nvm和n都是node.js版本管理工具&#xff0c;为了解决node.js各种版本存在不兼容现象可以通过它可以安装和切换不同版本的node.js。 nvm下载 可在点此在github上下载最新版本,本次…

拷贝构造函数(深拷贝+浅拷贝)

目录 拷贝构造函数浅拷贝深拷贝 拷贝构造函数 拷贝构造函数&#xff1a; Myclass(const Myclass& myclass) {amyclass.a;bmyclass.b;cmyclass.c; }浅拷贝 浅拷贝的思路就是和默认的拷贝构造函数一样: 即将原对象的值直接赋值给新对象&#xff0c;这样做一般情况下是没什…

加拿大儿童床垫认证标准要求介绍SOR/2016-152

儿童床垫是专为儿童设计的睡眠配件&#xff0c;用于提供舒适的睡眠环境和正确的睡眠支持 SOR/2016-152是加拿大政府发布的法规&#xff0c;旨在确保儿童床垫的安全性和质量。SOR/2016-152标准要求儿童床垫具有足够的安全性能&#xff0c;能够保护儿童免受潜在的危险和伤害。 标…

ArcGIS10.1软件安装教程

ArcGIS10.1中英文&#xff08;32/64位)下载地址&#xff1a; 链接&#xff1a; https://pan.baidu.com/s/1Ksm112WaKMMk6La9ircCng 密码&#xff1a;t70f 安装步骤&#xff1a; 1、我们对安装包进行解压&#xff0c;直接鼠标右击解压即可。 2、 打开我们解压的文件夹&#…

瞄准办公场景,未来智能靠“AI+耳机”后来居上?

如何在广阔红海中开拓出蓝海&#xff1f;未来智能或可作为参考案例。 作为TWS耳机玩家&#xff0c;未来智能成立于2021年&#xff0c;日前完成了由天际资本领投的数千万元Pre-A轮融资&#xff0c;这也是该公司成立以来完成的第二轮融资。 从成立时间来看&#xff0c;在广阔的…

Python学习之路 01如何安装Python

&#x1f600;前言 在当今数字化的时代&#xff0c;编程已成为一项基本技能。Python&#xff0c;因其简洁易学、功能强大和高度可扩展性而闻名&#xff0c;已成为许多初学者和专业开发人员的首选编程语言。不仅如此&#xff0c;Python拥有一个庞大而活跃的社群&#xff0c;提供…

科学中的人工智能:量子、原子和连续体技术概述

人工智能&#xff08;AI&#xff09;的进步正在推动自然科学领域的一种新的发现范式。如今&#xff0c;AI已经开始通过改进、加速和促进我们对各种空间和时间尺度上自然现象的理解来推动自然科学的发展&#xff0c;催生了一个被称为AI for science&#xff08;AI4Science&#…

SpringMVC之JSON数据返回及异常处理机制

目录 一.JSON数据的返回 二.异常处理机制 2.1 异常处理方式一 2.2 异常处理方式二 2.3 异常处理方式三 一.JSON数据的返回 JSON&#xff08;JavaScript Object Notation&#xff09;是一种轻量级的数据交换格式&#xff0c;常用于Web应用程序和服务之间的数据传输。通过使用…

MATLAB R2022b遇到“License Manager Error -8”怎么解决

电脑系统更新过后&#xff0c;matlab突然用不了了&#xff0c;报Error -8错误 将破解文件中的 libmwlmgrimpl.dll 复制 粘贴到 安装目录/R2022b/bin/win64/matlab_startup_plugins/lmgrimpl 直接替换 然后直接在 安装目录/R2022b/bin/ 打开matlab.exe

mingw 编译 curl ,Qt 工程使用

mingw 编译 curl 下载curl 源码 https://github.com/curl/curl 我使用8.3版 CMake-gui 配置 源码路径&#xff1a;D:/workspace/CPP/curl-8.3.0 生成路径: D:/workspace/CPP/curl-8.3.0/mingw-build 点击 Configure ,弹窗配置&#xff0c;选择 MinGW Makefiles 选择 Spec…

Codeforces Round 896 (Div. 2)

A.Make It Zero 思维题 当n为偶数时,对1到n异或使得1到n全部变成一样的数,再对1到n异或,使得全部变成0 当n为奇数时,对1到2异或使得1到2全部变成一样的,再对1到2异或,使得1到2都变成0,然后再两次对2到n AC代码: #include<iostream> #include<algorithm> #incl…

建筑模板国家标准GB/T 17656-2018

建筑模板国家标准GB/T 17656-2018《混凝土模板用胶合板》的重要性与应用 GB/T 17656-2018《混凝土模板用胶合板》国家标准是建筑模板行业的重要指导性文件&#xff0c;旨在确保混凝土模板的质量、安全和可靠性&#xff0c;促进建筑施工的高效进行。该标准详细规定了混凝土模板用…

DuDuTalk:4G语音工牌,如何实现家庭上门维修服务过程的智能化管理?

随着上门按摩、上门养老、上门买菜、上门维修等互联网上门服务的兴起&#xff0c;越来越多的居民开始采用线上下单&#xff0c;享受企业安排人员上门到家的服务。而家庭维修作为到家服务里面典型的一个场景&#xff0c;已成为许多人不可或缺的一部分。然而&#xff0c;与此同时…

【校招VIP】排序算法之高级排序

考点介绍&#xff1a; 在校招面试中,排序算法是经常被问到的。排序算法又比较多,很容易遗忘和混淆。有相当同学校招卡在排序的实现上,要么是核心代码实现不了,要么是实现方法串台。大厂的考察重点在快速排序等高级排序上。 排序算法之高级排序-相关题目及解析内容可点击文章末…

高级感满满的巡课效果怎么做?一招搞定!

学校是培养未来一代领袖和知识传承的地方&#xff0c;因此&#xff0c;确保学校内的教育质量和管理效率至关重要。在这个信息时代&#xff0c;技术正在不断演进&#xff0c;为学校管理和监督提供了更多的工具和方法。 在线巡课系统可以帮助学校管理人员更好地监督教学活动、评估…

【操作系统】进程的状态

进程的五种状态 创建&#xff0c;就绪&#xff08;等待分配处理机CPU资源&#xff09;&#xff0c;执行&#xff0c;阻塞&#xff08;叫阻塞因为相对CPU来说它很慢&#xff0c;等待除CPU以外的资源&#xff0c;如I/O&#xff09;&#xff0c;终止。 创建好的进程放入就绪队列等…

基于永磁同步发电机的风力发电系统研究(Simulink实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

MS SQL Server问题汇总

1.报SQL Server Agent连接不上的错误 15:38:57.991 [debezium-sqlserverconnector-sqlserver_transaction_log_source-change-event-source-coordinator] WARN i.d.connector.sqlserver.SqlServerStreamingChangeEventSource - No maximum LSN recorded in the database; pl…

mysql中left join时join和on的先后顺序

1.mysql中在内连接时,on的效果无论是对驱动表还是非驱动表和where的效果是一样的 ,on的顺序在join的前面 2.在外连接中 on 中添加筛选条件,只会对非驱动表生效,对驱动表是没有作用的,所以只有对非驱动表时,on的顺序是在join前面