【Mybatis】使用Mybatis参数拦截器实现数据加密存储及查询

实际的开发中,会有些字段值是不能明文存储在数据库的,例如密码、余额等。本文介绍的是通过Mybatis的参数拦截器实现无感知的数据加密存储及查询。

读懂本文你需要:会用SpringBoot  Mybatis,熟悉反射

依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<scope>runtime</scope>
</dependency>
<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
	<version>2.2.2</version>
</dependency>
<dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
	<optional>true</optional>
</dependency>
<dependency>
	<groupId>cn.hutool</groupId>
	<artifactId>hutool-all</artifactId>
	<version>5.7.12</version>
</dependency>

配置文件

#数据库配置
spring:
  datasource:
    # mysql8.0以下不带cj
    driver-class-name: com.mysql.cj.jdbc.Driver
    password: 111111
    url: jdbc:mysql://localhost:3306/crypt_demo?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
    username: root

#控制台日志
logging:
  level:
    com:
      example:
        mapper: debug

mybatis:
  #扫描mappers文件
  mapper-locations: classpath:mappers/*Mapper.xml
  #实体类和VO类起别名
  type-aliases-package: com.example.model
  #驼峰标识和数据库字段的相互自动转化
  configuration:
    map-underscore-to-camel-case: true

建表语句

create table user
(
    id       int auto_increment
        primary key,
    name     varchar(255) null,
    age      int          null,
    sex      int          null,
    password varchar(255) null,
    phone    varchar(16)  null
);

自定义注解

/**
 * 用在了类上的注解,有该注解表示该类对应的数据库表操作需要进行加解密
 *
 * @author weijd
 * @date 2022/3/23 13:37
 */
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptData {
}

/**
 * 用在属性上的注解,有该注解表示该属性对应的数据库表操作需要进行加解密
 *
 * @author weijd
 * @date 2022/3/23 13:37
 */
@Inherited
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField {
}

实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@EncryptData
public class User {
    /**
     * id
     */
    private int id;
    /**
     * 姓名
     */
    private String name;
    /**
     * 性别 1男 0女
     */
    private int sex;
    /**
     * 年龄
     */
    private int age;
    /**
     * 密码
     */
    @EncryptField
    private String password;
    /**
     * 手机
     */
    private String phone;
}

ServiceImpl

@Slf4j
@Service
public class UserServiceImpl implements UserService {
    @Resource
    private UserMapper userMapper;

    /**
     * 添加用户--注册
     *
     * @param user 用户
     * @return 大于0添加成功
     */
    @Override
    public int adduser(User user) {
        return userMapper.addUser(user);
    }

    /**
     * 查询用户
     *
     * @return User
     */
    @Override
    public User queryUser(User user) {
        User user1 = userMapper.queryUser(user);
        log.info(">>>>>>User:{}", user1);
        return user1;
    }
}

Controller

@RestController
@Slf4j
public class UserController {
    @Resource
    UserService userService;

    /**
     * 注册
     *
     * @param user 用户
     */
    @RequestMapping("/register")
    public Result<String> register(@RequestBody User user) {
        int i = userService.adduser(user);
        if (i == 0) {
            return Result.error(ClientCode.ERROR.getStatus(), "注册失败!");
        }
        return Result.success(ClientCode.SUCCESS.getStatus(), "注册成功!");
    }

    /**
     * 登录
     *
     * @param name     姓名
     * @param password 密码
     */
    @RequestMapping("/login")
    public Result<String> login(String name, String password) {
        User user = User.builder().name(name).password(password).build();
        if (userService.queryUser(user) == null) {
            return Result.error(ClientCode.ERROR.getStatus(), "登录失败!");
        }
        return Result.success(ClientCode.SUCCESS.getStatus(), "登录成功!");
    }
}

Mapper

public interface UserMapper {
    int addUser(User user);
    User queryUser(User user);
}

XML

<mapper namespace="com.example.mapper.UserMapper">
    <!--  根据用户名、密码查找用户(登录)  -->
    <select id="queryUser" parameterType="user" resultType="user">
        select *
        from user
        where name = #{name}
          and password = #{password}
    </select>

    <!--  添加用户  -->
    <insert id="addUser">
        insert into user
        values (0, #{name}, #{age}, #{sex}, #{password}, #{phone})
    </insert>
</mapper>

拦截器(核心)

① 写入数据拦截器

package com.example.intercept;

import com.example.annotion.EncryptData;
import com.example.annotion.EncryptField;
import com.example.util.EncryptUtil;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Properties;

@Component
@Intercepts(
        @Signature(type = ParameterHandler.class,
                method = "setParameters",
                args = PreparedStatement.class))
public class EncryptInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        ParameterHandler target = (ParameterHandler) invocation.getTarget();
        Field parameterObject = target.getClass().getDeclaredField("parameterObject");
        parameterObject.setAccessible(true);
        Object o = parameterObject.get(target);
        List<Field> encryptFields = new ArrayList<>();
        if (!Objects.isNull(o)) {
            Class<?> aClass = o.getClass();
            // 如果有EncryptData注解
            if (aClass.isAnnotationPresent(EncryptData.class)) {
                Field[] declaredFields = aClass.getDeclaredFields();
                for (Field field : declaredFields) {
                    // 如果属性带有EncryptField注解放到要加解密的集合中
                    if (field.isAnnotationPresent(EncryptField.class)) {
                        encryptFields.add(field);
                    }
                }
                //调用工具类进行加密
                EncryptUtil.encrypt(encryptFields, o);
            }
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        Interceptor.super.setProperties(properties);
    }
}

② 读取数据拦截器

package com.example.intercept;

import com.example.annotion.EncryptData;
import com.example.util.DecryptUtil;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.Component;
import java.sql.Statement;
import java.util.List;
import java.util.Objects;
import java.util.Properties;

@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 proceed = invocation.proceed();
        if (Objects.isNull(proceed)) {
            return null;
        }
        if (proceed instanceof List) {
            List list = (List) proceed;
            if (!list.isEmpty()) {
                if (list.get(0).getClass().isAnnotationPresent(EncryptData.class)) {
                    for (Object o : list) {
                        //解密工具类解密
                        DecryptUtil.decrypt(o);
                    }
                }
            }
        } else {
            if (proceed.getClass().isAnnotationPresent(EncryptData.class)) {
                DecryptUtil.decrypt(proceed);
            }
        }
        return proceed;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        Interceptor.super.setProperties(properties);
    }
}

EncryptUtil

package com.example.util;

import com.example.annotion.EncryptField;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.List;

@Slf4j
@Component
public class EncryptUtil {

    public static <T> void encrypt(List<Field> fields, T params) throws Exception {
        for (Field field : fields) {
            // 判断属性是否有EncryptField注解
            if (field.isAnnotationPresent(EncryptField.class)) {
                // 设置私有属性可访问
                field.setAccessible(true);
                Object o = field.get(params);
                if (o instanceof String) {
                    String value = (String) o;
                    log.info(">>>>>>加密字段:{}", field.getName());
                    //对值进行加密,接着重新给属性赋值,这样插入数据库的数据值就是加密后的值
                    field.set(params, AesUtil.encrypt(value));
                }
            }
        }
    }
    
}

DecryptUtil

package com.example.util;

import com.example.annotion.EncryptField;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;

@Slf4j
@Component
public class DecryptUtil {

    public static <T> void decrypt(T result) throws Exception {
        Class<?> aClass = result.getClass();
        // 拿到该类所有的属性,不包括父类的
        Field[] declaredFields = aClass.getDeclaredFields();
        for (Field field : declaredFields) {
            // 判断属性是否有EncryptField注解
            if (field.isAnnotationPresent(EncryptField.class)) {
                // 设置私有属性可访问
                field.setAccessible(true);
                Object o = field.get(result);
                if (o instanceof String) {
                    String value = (String) o;
                    log.info(">>>>>>解密字段:{}", field.getName());
                    //对查询出来的值进行解密,接着重新给属性赋值
                    field.set(result, AesUtil.decrypt(value));
                }
            }
        }
    }

}

AesUtil

加解密用的是AES对称加密,注意下面KEY 的长度(只能16、24、32)!不然会报一个错误:

java.security.InvalidKeyException: Invalid AES key length: 13 bytes

查到的原因是:在AES的规格中,密钥长度只有128、192和256比特三种。

package com.example.util;

import cn.hutool.crypto.SecureUtil;

public class AesUtil {
    /**
     * 对称加密秘钥,注意秘钥这里的长度只能是16,24或32
     */
    private static final String KEY = "YOUR KEY YOUR KE";

    /**
     * 加密
     *
     * @param text 要加密字符串
     * @return 加密过的字符串
     */
    public static String encrypt(String text) {
        return SecureUtil.aes(KEY.getBytes()).encryptHex(text);
    }

    /**
     * 解密
     *
     * @param text 要解密字符串
     * @return 解密过的字符串
     */
    public static String decrypt(String text) {
        return SecureUtil.aes(KEY.getBytes()).decryptStr(text);
    }

}

除此之外,你也可以采用别的加密方式,比如MD5,DES,Base64…
严格意义来说,Base64只是把内容换成你看不懂的东西而已,不是加密。在这里插入图片描述

测试结果

测试JSON:

{
    "name": "CSDN 彩虹丶",
    "sex": 1,
    "age": 24,
    "password": "111111",
    "phone": "15624560000"
}

在这里插入图片描述

控制台打印:

2024-01-25 16:34:38.623 DEBUG 29180 --- [nio-8080-exec-1] com.example.mapper.UserMapper.addUser    : ==>  Preparing: insert into user values (0, ?, ?, ?, ?, ?)
2024-01-25 16:34:38.636  INFO 29180 --- [nio-8080-exec-1] com.example.util.EncryptUtil             : >>>>>>加密字段:password
2024-01-25 16:34:38.643 DEBUG 29180 --- [nio-8080-exec-1] com.example.mapper.UserMapper.addUser    : ==> Parameters: CSDN 彩虹丶(String), 24(Integer), 1(Integer), 457ea075c8280d71a3b82960090c1c32(String), 15624560000(String)
2024-01-25 16:34:38.651 DEBUG 29180 --- [nio-8080-exec-1] com.example.mapper.UserMapper.addUser    : <==    Updates: 1

可以看到insert SQL执行时的password参数由我们输入的111111变为了一串加密过后的字符串。来看看数据库,存入了加密后的password。
在这里插入图片描述

接着我们登录(查询):
在这里插入图片描述

2024-01-25 16:39:51.529 DEBUG 29180 --- [nio-8080-exec-3] com.example.mapper.UserMapper.queryUser  : ==>  Preparing: select * from user where name = ? and password = ?
2024-01-25 16:39:51.530  INFO 29180 --- [nio-8080-exec-3] com.example.util.EncryptUtil             : >>>>>>加密字段:password
2024-01-25 16:39:51.530 DEBUG 29180 --- [nio-8080-exec-3] com.example.mapper.UserMapper.queryUser  : ==> Parameters: CSDN 彩虹丶(String), 457ea075c8280d71a3b82960090c1c32(String)
2024-01-25 16:39:51.544 DEBUG 29180 --- [nio-8080-exec-3] com.example.mapper.UserMapper.queryUser  : <==      Total: 1
2024-01-25 16:39:51.546  INFO 29180 --- [nio-8080-exec-3] com.example.util.DecryptUtil             : >>>>>>解密字段:password
2024-01-25 16:39:51.552  INFO 29180 --- [nio-8080-exec-3] c.example.service.impl.UserServiceImpl   : >>>>>>UserUser(id=5, name=CSDN 彩虹丶, sex=1, age=24, password=111111, phone=15624560000)

查询的时候也会进行拦截,把输入的参数加密后再去数据库查询,查询出来的数据经过解密后,可以看到password和输入时的是一致的。

在这里插入图片描述
散会!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

彩虹、

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

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

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

打赏作者

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

抵扣说明:

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

余额充值