MyBatis-Plus拦截器对敏感数据实现加密

目录
  • 一、定义注解
  • 二、定义拦截器类

做课程项目petstore时遇到需要加密属性的问题,而MyBatis-Plus为开发者提供了拦截器的相关接口,用于与数据库交互的过程中实现特定功能,本文主要介绍通过MyBatis-Plus的拦截器接口自定义一个拦截器类实现敏感数据如用户密码的加密功能,即实现在DAO层写入数据库时传入明文,而数据库中存储的是密文。由于加密算法有多种,这里不展示具体的加密步骤,主要讨论拦截器的构建。

一、定义注解

自定义相关注解,将需要加密的字段及其所在的实体类进行标注,方便拦截器拦截时的判断。这里定义了两个注解(分别形成两个不同的文件),@SensitiveData和@SensitiveField,分别用于注解实体类、实体类中需要加密的属性。注意,注解@Target内ElementType取值为TYPE时表示该注解用于注解类,取值为FIELD表示该注解用于注解类的属性。

package org.csu.mypetstore.api.utils.encrypt.annotation;

import java.lang.annotation.*;

@Inherited
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
}

二、定义拦截器类

自定义的拦截器类需要实现MyBatis的Interceptor类,主要是重写三个方法public Object intercept(Invocation invocation)public Object plugin(Object target)public void setProperties(Properties properties)。这三个方法前两个我们需要使用到,第三个方法这里暂时用不到。

为我们创建的拦截器类打上@Component注解使之被Spring容器所管理;打上@Intercepts注解用于标识拦截器开始拦截的情况(执行sql语句过程中的哪个位置)。Mybatis可以在执行语句的过程中对特定对象进行拦截调用,主要有以下四种情况:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) 处理增删改查
  • ParameterHandler (getParameterObject, setParameters) 设置预编译参数
  • ResultSetHandler (handleResultSets, handleOutputParameters) 处理结果
  • StatementHandler (prepare, parameterize, batch, update, query) 处理sql预编译,设置参数

通过@Intercepts注解内部的@Signature注解进行配置,有三个配置选项分别为type、method、args,type用于指定上述四类中的某一类,method用于指定该类型中的哪个方法执行时被拦截,args用于接收被拦截方法的参数。观察注解@Signature的代码可以加深理解:

这里选取ParameterHandler类型的setParameters方法,在每次执行sql之前设置参数前进行拦截加密。整个拦截器类重写intercept方法用于加密、重写plugin方法用于将该拦截器接入拦截器链(这里可以选择不重写plugin方法,因为Interceptor类中定义的该方法默认内容与我们重写的内容是一样的),代码如下:

package org.csu.mypetstore.api.utils.encrypt;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.plugin.*;
import org.csu.mypetstore.api.utils.encrypt.annotation.SensitiveData;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;

/**
 * 加密拦截器:插入数据库之前对敏感数据加密
 * 场景:插入、更新时生效
 * 策略:
 *   - 在敏感字段所在实体类上添加@SensitiveData注解
 *   - 在敏感字段上添加@SensitiveField注解
 *
 * @author csu_cangkui
 * @date 2021/8/14
 */
@Slf4j
@Component
@Intercepts({
        @Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class)
})
public class EncryptInterceptor implements Interceptor {

    // 更新时的参数名称,ParamMap的key值
    private static final String CRYPT = "et";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            // @Signature 指定了 type= parameterHandler.class 后,这里的 invocation.getTarget() 便是parameterHandler
            // 若指定ResultSetHandler,这里则能强转为ResultSetHandler
            ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
            // 获取参数对像,即对应MyBatis的 mapper 中 paramsType 的实例
            Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
            parameterField.setAccessible(true);
            // 取出参数实例
            Object parameterObject = parameterHandler.getParameterObject();
            if (parameterObject != null) {
                Object sensitiveObject = null;
                if (parameterObject instanceof MapperMethod.ParamMap) {
                    // 更新操作被拦截
                    Map paramMap = (Map) parameterObject;
                    sensitiveObject = paramMap.get(CRYPT);
                } else {
                    // 插入操作被拦截,parameterObject即为待插入的实体对象
                    sensitiveObject = parameterObject;
                }
                // 获取不到数据就直接放行
                if (Objects.isNull(sensitiveObject)) return invocation.proceed();
                // 校验该实例的类是否被@SensitiveData所注解
                Class<?> sensitiveObjectClass = sensitiveObject.getClass();
                SensitiveData sensitiveData = AnnotationUtils.findAnnotation(sensitiveObjectClass, SensitiveData.class);
                if (Objects.nonNull(sensitiveData)) {
                    // 如果是被注解的类,则进行加密
                    // 取出当前当前类所有字段,传入加密方法
                    Field[] declaredFields = sensitiveObjectClass.getDeclaredFields();
                    EncryptUtil.encrypt(declaredFields, sensitiveObject, EncryptUtil.ENCRYPT_MD5_MODE);
                }
            }
            return invocation.proceed();
        } catch (Exception e) {
            // 未作更多处理,加密失败仍然会放行让数据进入数据库
            log.error("加密失败", e);
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        // 将这个拦截器接入拦截器链
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {}
}

EncryptUtil类的encrypt加密方法的大致处理流程:

static <T> T encrypt(Field[] declaredFields, T sensitiveObject, final int ENCRYPT_MODE) throws IllegalAccessException {
    for (Field field : declaredFields) {
        // 取出所有被SensitiveField注解的字段
        SensitiveField sensitiveField = field.getAnnotation(SensitiveField.class);
        if (Objects.nonNull(sensitiveField)) {
            field.setAccessible(true);
            Object targetProperty = field.get(sensitiveObject);
            // 仅讨论对字符串类型的字段的加密
            if (targetProperty instanceof String) {
                // 取得源字段值
                String value = (String) targetProperty;
                String valueEncrypt;
                if (ENCRYPT_MODE == ENCRYPT_MD5_MODE) {
                    // 使用MD5加密算法进行加密
                    valueEncrypt = EncryptUtil.MD5Encrypt(value);
                } else if (ENCRYPT_MODE == ENCRYPT_AES_MODE) {
                    // 使用AES加密算法进行加密
                    valueEncrypt = EncryptUtil.AESEncrypt(value);
                } else {
                    valueEncrypt = value;
                }
                // 将加密完成的字段值放入待用参数对象
                field.set(sensitiveObject, valueEncrypt);
            }
        }
    }
    return sensitiveObject;
}

构建过程中主要是使用了Java的反射机制来处理。在构建的过程中需要注意的一点是,我们需要在更新、插入时进行加密,但是网上很多方法都是默认为插入时加密,所以取出来的parameterObject对象都是默认为这个表对应的实体类。但是更新操作不同,更新操作时打印parameterObject对象的类型可看到是org.apache.ibatis.binding.MapperMethod$ParamMap,即取出的parameterObject对象是一个ParamMap类,而其中"et"的key对应的value才是我们需要的实体类,因此这里需要通过判断parameterObject对象的类型来分类进行处理。

如果选用的是双向加密算法(可逆),还可以设计一个用于解密的拦截器类进行处理,选取ResultSetHandler类型的handleResultSets方法,在处理结果集之前进行解密,解密的情况根据具体需求来确定,可以基于selectList方法、基于selectOne方法等等。

package org.csu.mypetstore.api.utils.encrypt;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.csu.mypetstore.api.utils.encrypt.annotation.SensitiveData;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;

import java.beans.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Properties;

@Slf4j
@Component
@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();
        try {
            if (Objects.isNull(resultObject)) return null;
            if (resultObject instanceof ArrayList) {
                // 基于selectList
                List resultList = (ArrayList) resultObject;
                if (!resultList.isEmpty() && needToDecrypt(resultList.get(0))) {
                    for (Object result : resultList) {
                        //逐一解密
                        EncryptUtils.decrypt(result);
                    }
                }
            } else {
                // 基于selectOne
                if (needToDecrypt(resultObject)) EncryptUtils.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 target) {
        // 将这个拦截器接入拦截器链
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {}
}

到此这篇关于MyBatis-Plus拦截器对敏感数据实现加密的文章就介绍到这了,更多相关MyBatis-Plus拦截器对敏感数据加密内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • MyBatis-Plus如何实现自动加密解密

    目录 MyBatis-Plus 自动加密解密 实现TypeHandler 添加注解 查询加密字段 MyBatis-Plus 敏感数据的加密 写加密解密的工具类 继承BaseTypeHandler ,实现对数据的转换 有po类中,实现相关类型注解 MyBatis-Plus 自动加密解密 通过使用MyBatis的typeHandler功能,对入参和出参进行处理,实现无缝加密解密(将明文加密后保存至数据库:从数据库读取时,自动将密文解密成明文) 实现TypeHandler @Slf4j public

  • MyBatis-Plus拦截器对敏感数据实现加密

    目录 一.定义注解 二.定义拦截器类 做课程项目petstore时遇到需要加密属性的问题,而MyBatis-Plus为开发者提供了拦截器的相关接口,用于与数据库交互的过程中实现特定功能,本文主要介绍通过MyBatis-Plus的拦截器接口自定义一个拦截器类实现敏感数据如用户密码的加密功能,即实现在DAO层写入数据库时传入明文,而数据库中存储的是密文.由于加密算法有多种,这里不展示具体的加密步骤,主要讨论拦截器的构建. 一.定义注解 自定义相关注解,将需要加密的字段及其所在的实体类进行标注,方便拦

  • Mybatis自定义拦截器和插件开发详解

    前言 在Spring中我们经常会使用到拦截器,在登录验证.日志记录.性能监控等场景中,通过使用拦截器允许我们在不改动业务代码的情况下,执行拦截器的方法来增强现有的逻辑.在mybatis中,同样也有这样的业务场景,有时候需要我们在不侵入原有业务代码的情况下拦截sql,执行特定的某些逻辑.那么这个过程应该怎么实现呢,同样,在mybatis中也为开发者预留了拦截器接口,通过实现自定义拦截器这一功能,可以实现我们自己的插件,允许用户在不改动mybatis的原有逻辑的条件下,实现自己的逻辑扩展. 本文将按

  • MyBatis Excutor 拦截器的巧妙用法

    这里要讲的巧妙用法是用来实现在拦截器中执行额外 MyBatis 现有方法的用法. 并且会提供一个解决拦截Executor时想要修改MappedStatement时解决并发的问题. 这里假设一个场景: 实现一个拦截器,记录 MyBatis 所有的 insert,update,delete 操作,将记录的信息存入数据库. 这个用法在这里就是将记录的信息存入数据库. 实现过程的关键步骤和代码: 1.首先在某个 Mapper.xml 中定义好了一个往日志表中插入记录的方法,假设方法为id="insert

  • Mybatis中拦截器的简单实现方法

    前言 需求驱动学习,最近一周组长让我在业务模块里加日志,经过与导师以及组长讨论决定用拦截器记录日志.周五下班前已经发了提测邮件. 虽然我知道 MyBatis 有这东西,但是没在实际情况中用过,心里有点虚2333--所以才有了此文的理解. 前世今生 它的本质就是 JDK 的动态代理.首先先来复习一下动态代理我贴了一段最常见的 JDK 动态代理的代码 //服务员的接口 public interface Waiter { void serve(); } //服务员的实现 public class Wa

  • Mybatis Plugin拦截器开发过程详解

    这篇文章主要介绍了Mybatis Plugin拦截器开发过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 1.Plugin MyBatis 允许使用插件来拦截的方法调用包括: • Executor (update, query, flushStatements, commit, rollback,getTransaction, close, isClosed) • ParameterHandler (getParameterObject,

  • mybatis 通过拦截器打印完整的sql语句以及执行结果操作

    开发过程中,如果使用mybatis做为ORM框架,经常需要打印出完整的sql语句以及执行的结果做为参考. 虽然mybatis结合日志框架可以做到,但打印出来的通常都是sql和参数分开的. 有时我们需要调试这条sql的时候,就需要把参数填进去,这样未免有些浪费时间. 此时我们可以通过实现mybatis拦截器来做到打印带参数的完整的sql,以及结果通过json输出到控制台. 直接看代码和使用方法吧: MyBatis拦截器打印不带问号的完整sql语句拦截器 import java.text.DateF

  • springboot整合mybatis分页拦截器的问题小结

    简介 又到了吹水时间,是这样的,今天开发时想将自己写好的代码拿来优化,因为不想在开发服弄,怕搞坏了到时候GIT到生产服一大堆问题,然后把它分离到我轮子(工具)项目上,最后运行后发现我获取List的时候很卡至少10秒,我惊了平时也就我的正常版本是800ms左右(不要看它很久,因为数据量很大,也很正常.),前提是我也知道很慢,就等的确需要优化时,我在放出我优化的plus版本,回到10秒哪里,最开始我刚刚接到这个app项目时,在我用 PageHelper.startPage(page, num);(分

  • SpringBoot整合Mybatis自定义拦截器不起作用的处理方案

    目录 SpringBoot整合Mybatis自定义拦截器不起作用 1. 原始的读取mybatis-config.xml文件 2. 与SpringBoot容器整合 2.1 mybatis的自动装载 3. 在mybatis-config.xml配置又放入Spring容器 SpringBoot 自定义Mybatis拦截器 第一种 第二种 第三种 SpringBoot整合Mybatis自定义拦截器不起作用 Mybatis插件生效的方式: 1. 原始的读取mybatis-config.xml文件 该方式和

  • 在springboot中如何给mybatis加拦截器

    目录 1.实现Interceptor接口,并添加拦截注解 @Intercepts 1.在mybatis中可被拦截的类型有四种(按照拦截顺序) 2.各个参数的含义 2.在配置文件中添加拦截器 (1)第一种 (2)第二种 (3)第三种 拦截器的作用就是我们可以拦截某些方法的调用,在目标方法前后加上我们自己逻辑 Mybatis拦截器设计的一个初衷是为了供用户在某些时候可以实现自己的逻辑而不必去动Mybatis固有的逻辑. mybatis 自定义拦截器 1.实现Interceptor 接口,并添加拦截注

  • Mybatis Interceptor 拦截器的实现

    Mybatis采用责任链模式,通过动态代理组织多个拦截器(插件),通过这些拦截器可以改变Mybatis的默认行为(诸如SQL重写之类的),由于插件会深入到Mybatis的核心,因此在编写自己的插件前最好了解下它的原理,以便写出安全高效的插件. 拦截器(Interceptor)在 Mybatis 中被当做插件(plugin)对待,官方文档提供了 Executor,ParameterHandler,ResultSetHandler,StatementHandler 共4种,并且提示"这些类中方法的细

随机推荐