【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
  • 并调用这个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());
    }
}

单元测试运行截图

在这里插入图片描述

  • 8
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: Mybatis-PlusMybatis的一个增强工具,它可以优化我们的开发效率。在实际的项目开发中,我们通常需要编写复杂的SQL查询语句来满足业务需求。下面我将介绍如何在自定义的Mapper类中实现复杂的SQL查询操作。 Mybatis-Plus提供了很多基础的查询方法,比如新增、修改、删除、查询全部等,但是当我们遇到一些比较复杂的查询需求时,需要自己手动编写SQL语句。我们可以在自定义的Mapper接口中定义SQL查询方法,然后在XML文件中编写SQL语句,最后通过Mybatis-Plus注解进行映射。 首先,在自定义的Mapper接口中定义一个查询方法,比如:selectUserList。在这个方法上使用注解@Select,用于映射XML文件中的SQL语句。在这个方法的参数中,我们可以传入一些查询条件用于过滤查询结果,比如用户姓名、年龄等信息。如果需要分页查询,我们可以传入Page对象,然后在XML文件中使用<if>标签判断是否需要拼装分页的SQL语句。 然后,在XML文件中编写SQL语句。针对不同的查询需求,我们可以使用各种关键字、函数、运算符等语法进行拼装。在使用变量的时候,需要使用#{XXX}形式的占位符来代替变量,同时也可以使用${XXX}形式的占位符来代替SQL关键字、表名等信息。 最后,在Mapper接口上使用@Mapper注解将这个接口进行映射,然后在Service层中调用这个接口中定义的查询方法即可。如果需要进行分页查询,我们需要手动创建一个Page对象,并设置分页信息,然后将这个对象传入到Mapper接口中即可。 总之,对于比较复杂的SQL查询操作,我们可以通过自定义Mapper接口、XML文件以及Mybatis-Plus注解的方式来实现。这样可以大大提升我们的查询效率和开发效率,减少我们的工作量和出错的概率。 ### 回答2: MyBatis-Plus 是一个 Mybatis 的增强工具,在持久层操作方面做了很多增强和优化,其中包括自定义复杂 SQL 查询。 实现自定义复杂 SQL 查询的步骤如下: 1. 在实体类中添加查询参数的字段,如下: ``` public class User { private Integer id; private String name; private Integer age; private String phone; // getter and setter ... } ``` 2. 在 mapper.xml 中编写自定义复杂 SQL 查询语句: ``` <select id="selectByCustomQuery" resultMap="BaseResultMap"> SELECT id,name,age,phone FROM user <where> <if test="name!=null"> and name like concat('%', #{name}, '%') </if> <if test="age!=null"> and age = #{age} </if> <if test="phone!=null"> and phone like concat('%', #{phone}, '%') </if> </where> </select> ``` 3. 在 mapper 接口中添加自定义查询的方法: ``` public interface UserMapper extends BaseMapper<User> { List<User> selectByCustomQuery(@Param("name") String name, @Param("age") Integer age, @Param("phone") String phone); } ``` 4. 在 service 层中调用自定义查询的方法: ``` @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public List<User> findByCustomQuery(String name, Integer age, String phone) { return userMapper.selectByCustomQuery(name, age, phone); } } ``` 最后,调用 findByCustomQuery 方法可以实现自定义复杂 SQL 查询。 以上是实现自定义复杂 SQL 查询的简单步骤,需要注意的是在 XML 中编写 SQL 语句时,需要加入防 SQL 注入的措施。 ### 回答3: MyBatis-Plus是一款基于MyBatis的增强工具包,它封装了很多MyBatis的常用操作,例如:分页查询、自动逆向工程、注解CRUD、性能分析等,其中自定义复杂SQL查询也得到了很好的支持。 MyBatis-Plus自定义复杂SQL语句的步骤如下: 1.定义Mapper接口 在Mapper接口中定义自定义查询方法,例如: ```java @Select("SELECT * FROM user WHERE age > #{age}") List<User> selectByAge(Integer age); ``` 2.使用MyBatis-Plus提供的BaseMapper 在Mapper接口中继承MyBatis-Plus提供的BaseMapper,并使用@Mapper注解标记接口。 ```java @Mapper public interface UserMapper extends BaseMapper<User> { @Select("SELECT * FROM user WHERE age > #{age}") List<User> selectByAge(Integer age); } ``` 3.使用XML方式实现自定义查询 如果自定义查询语句比较复杂,可以使用XML方式实现。在Mapper接口中定义方法,例如: ```java List<UserVO> selectUserVO(); ``` 在resources/mapper/UserMapper.xml中实现自定义sql语句,例如: ```xml <select id="selectUserVO" resultMap="userVOResultMap"> SELECT u.*, d.name AS deptName FROM user u LEFT JOIN department d ON u.dept_id = d.id </select> ``` 4.在Service层调用Mapper接口中的自定义方法 在Service层中注入Mapper,并调用Mapper接口中的自定义方法,例如: ```java @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public List<UserVO> selectUserVO() { return userMapper.selectUserVO(); } } ``` 以上就是使用MyBatis-Plus实现自定义复杂SQL查询的步骤,它可以很好地帮助我们提高数据查询的效率和灵活性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

太空眼睛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值