MyBatis-Plus拦截器对敏感数据实现加密
作者:csu_cangkui
做课程项目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拦截器对敏感数据加密内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!