Mybatis-Plus根据自定义注解实现自动加解密的示例代码
作者:太空眼睛
背景
我们把数据存到数据库的时候,有些敏感字段是需要加密的,从数据库查出来再进行解密。如果存在多张表或者多个地方需要对部分字段进行加解密操作,每个地方都手写一次加解密的动作,显然不是最好的选择。如果我们使用的是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 - 并调用这个
Bean
的addInnerInterceptor
方法,把所有的InnerInterceptor
加入进去,才能生效 InnerInterceptor
只有before
拦截,缺省after
拦截。加密可以在before
里面完成,但解密需要在after
里面完成,所以这个InnerInterceptor
不能满足我们的要求
所以继续研究源码,发现Mybatis
有个org.apache.ibatis.plugin.Interceptor
接口,这个接口能满足我对自动加解密的所有诉求
- 首先,实现
Interceptor
接口,只要注册成为Spring
容器的Bean
,拦截器就能生效 - 可以更加灵活的在
before
和after
之间插入自己的逻辑
加密拦截器
创建名为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()); } }
单元测试运行截图
以上就是Mybatis-Plus根据自定义注解实现自动加解密的示例代码的详细内容,更多关于Mybatis-Plus自定义注解加解密的资料请关注脚本之家其它相关文章!