原生mybatis实现数据加密存储和读取

原生mybatis实现数据加密存储和读取

物料准备:

1.mysql,mybatis等依赖

2.定义数据库表以及对应的Java实体类、mapper接口/xml等代码

3.定义加密类和字段的Java注解

4.定义mybatis拦截器插件以及数据加解密工具类

5.测试数据插入和查询的效果

pom.xml中引入相关依赖

 <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.8</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>
         <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

定义数据库表以及对应的Java实体类mapper接口xml等代码

数据库表SQL代码

CREATE DATABASE test619; 
ALTER DATABASE `test619` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE `test619`; 
CREATE TABLE `t_account` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(255) NOT NULL,
  `password_ciper` VARCHAR(255) NOT NULL DEFAULT '1',
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

java实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@EncryptDecryptClass
public class Account implements Serializable {
    private static final long serialVersionUID = -8114863398578320034L;
    private Integer id;
    private String username;

    @EncryptDecryptField
    private String passwordCiper;
}

对应的mapper接口以及xml

package com.example.demo.orm.dao;

import com.example.demo.orm.po.Account;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;

@Mapper
public interface AccountMapper {
    Account findOneById(@Param("id") Integer id);

    List<Account> selectUserList(Account account);

    int insertOne(Account account);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.orm.dao.AccountMapper">
<resultMap id="BaseResultMap"
               type="com.example.demo.orm.po.Account">
        <id column="id" jdbcType="BIGINT" property="id"/>
        <result column="username" jdbcType="VARCHAR" property="username"/>       
        <result column="password_ciper" jdbcType="VARCHAR" property="passwordCiper"/>
    </resultMap>  
    
    <select id="findOneById" resultMap="BaseResultMap">
        select * from t_account where id = #{id}
    </select>
    
    <select id="selectUserList"
            parameterType="com.example.demo.orm.po.Account"
            resultMap="BaseResultMap">
        select * from t_account
        <where>
            <if test="id != null">
                and id = #{id}
            </if>
            <if test="username != null and username != '' ">
                and username = #{username}
            </if>         
            <if test="passwordCiper != null and passwordCiper != '' ">
                and password_ciper = #{passwordCiper}
            </if>
        </where>

    </select>
    
    <insert id="insertOne"
            useGeneratedKeys="true"
            keyProperty="id"
            parameterType="com.example.demo.orm.po.Account">
       insert into t_account(username,password_ciper)
        values (#{username},#{passwordCiper})
    </insert>
    
 </mapper>   

springboot项目的mybatis配置

yml配置mybatis

server:
  port: 8615

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/test805?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&allowMultiQueries=true
    username: root
    password: 123456
mybatis:
  # 实体类所在的包
  type-aliases-package: com.example.demo.orm.po
  # *Mapper.xml文件放在resource下的mappers目录里
  mapper-locations: classpath:mappers/*Mapper.xml
  configuration:
    # 打印mybatis执行的日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    #数据库下划线名 转 java代码小驼峰
    map-underscore-to-camel-case: true
    # 开启全局缓存(mybatis二级缓存),默认不会开启,需要程序员配置开启
    cache-enabled: true

项目启动类上配置mybatis注解

package com.example.demo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;

//扫描mybatis的java接口所在的包
@MapperScan(basePackages = {"com.example.demo.orm.dao"})
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

定义加密类和字段的Java注解

定义EncryptDecryptClass.java

package com.example.demo.anot;
import java.lang.annotation.*;

//定义注解@EncryptDecryptClass,用于标记需要加解密的java类
@Inherited
@Target({ ElementType.TYPE })@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptDecryptClass {
}

定义EncryptDecryptField.java

package com.example.demo.anot;
import java.lang.annotation.*;
//定义注解@EncryptDecryptField,用于标记需要加解密的java字段
@Inherited
@Target({ ElementType.FIELD })@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptDecryptField {
}

定义mybatis拦截器插件以及数据加解密工具类

定义数据加解密工具类EncryptDecryptUtil.java

package com.example.demo.plugin;
import cn.hutool.crypto.SecureUtil;
import com.example.demo.anot.EncryptDecryptField;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;
import java.util.Objects;

@Slf4j
public class EncryptDecryptUtil {
     /**
     * 对称加密的秘钥
     */
    private final static String key = "asffqqas";
    /**
     * * 加密     *
     * * @param declaredFields paramsObject所声明的字段
     * * @param paramsObject   mapper中paramsType的实例
     * * @return T     * @throws IllegalAccessException 字段不可访问异常
     */
    public static <T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException {
        for (Field field : declaredFields) {            //取出所有被EncryptDecryptField注解的字段
            EncryptDecryptField sensitiveField = field.getAnnotation(EncryptDecryptField.class);
            if (!Objects.isNull(sensitiveField)) {
                field.setAccessible(true);
                Object object = field.get(paramsObject);                //暂时只实现String类型的加密
                if (object instanceof String) {
                    String value = (String) object;                    //加密  Des加密工具
                    String encryptHexStr = SecureUtil.des(key.getBytes()).encryptHex(value);
                    field.set(paramsObject, encryptHexStr);
                }
            }
        }
        return paramsObject;
    }
    /**
     * 解密     *     * @param result resultType的实例     * @return T     * @throws IllegalAccessException 字段不可访问异常
     */
    public static <T> T decrypt(T result) throws IllegalAccessException {
        //取出resultType的类
        Class<?> resultClass = result.getClass();
        Field[] declaredFields = resultClass.getDeclaredFields();
        for (Field field : declaredFields) {
            //取出所有被EncryptDecryptField注解的字段
            EncryptDecryptField sensitiveField = field.getAnnotation(EncryptDecryptField.class);
            if (!Objects.isNull(sensitiveField)) {
                field.setAccessible(true);
                Object object = field.get(result);                //只支持String的解密
                if (object instanceof String) {
                    String value = (String) object;
                    //对注解的字段进行逐一解密
                    String decryptStr = SecureUtil.des(key.getBytes()).decryptStr(value);
                    log.info("{}字段需要解密,{}解密后的值是{}",field.getName(),value,decryptStr);
                    field.set(result, decryptStr);
                }
            }
        }
        return result;
    }
}

定义和配置自定义的mybatis读写拦截器

读取数据拦截器ReadInterceptor.java

package com.example.demo.plugin;

import com.example.demo.anot.EncryptDecryptClass;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.CollectionUtils;

import java.sql.Statement;
import java.util.ArrayList;
import java.util.Objects;

@Intercepts({
        @Signature(type = ResultSetHandler.class,
                method = "handleResultSets",
                args = {Statement.class})})
public class ReadInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //取出查询的结果
        Object resultObject = invocation.proceed();
        if (Objects.isNull(resultObject)) {
            return null;
        }
        //基于selectList
        if (resultObject instanceof ArrayList) {
            ArrayList resultList = (ArrayList) resultObject;
            if (!CollectionUtils.isEmpty(resultList) && needToDecrypt(resultList.get(0))) {
                for (Object result : resultList) {
                    //逐一解密
                    EncryptDecryptUtil.decrypt(result);
                }
            }
            //基于selectOne
        } else {
            if (needToDecrypt(resultObject)) {
                EncryptDecryptUtil.decrypt(resultObject);
            }
        }
        return resultObject;
    }

    private boolean needToDecrypt(Object object) {
        Class<?> objectClass = object.getClass();
        EncryptDecryptClass sensitiveData =
                AnnotationUtils.findAnnotation(objectClass, EncryptDecryptClass.class);
        return Objects.nonNull(sensitiveData);
    }
    
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    
}

写入数据拦截器WriteInterceptor.java

package com.example.demo.plugin;

import com.example.demo.anot.EncryptDecryptClass;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.core.annotation.AnnotationUtils;

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


@Intercepts({
        @Signature(type = ParameterHandler.class,
                method = "setParameters",
                args = PreparedStatement.class)
})
public class WriteInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //@Signature 指定了 type= parameterHandler 后,
        // 这里的 invocation.getTarget() 便是parameterHandler
        // 若指定ResultSetHandler ,这里则能强转为ResultSetHandler
        ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
        // 获取参数对像,即 mapper 中 paramsType 的实例
        Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
        parameterField.setAccessible(true);        //取出实例
        Object parameterObject = parameterField.get(parameterHandler);
        if (parameterObject != null) {
            Class<?> parameterObjectClass = parameterObject.getClass();

            //校验该实例的类是否被EncryptDecryptClass所注解
            EncryptDecryptClass encryptDecryptClass =
                    AnnotationUtils.findAnnotation(parameterObjectClass, EncryptDecryptClass.class);
            if (Objects.nonNull(encryptDecryptClass)) {
                //取出当前当前类所有字段,传入加密方法
                Field[] declaredFields = parameterObjectClass.getDeclaredFields();
                EncryptDecryptUtil.encrypt(declaredFields, parameterObject);
            }
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object o) {
        //这里必须写入,会判定是否把当前拦截器启动
        return Plugin.wrap(o, this);
    }
    
}

配置自定义的mybatis拦截器

package com.example.demo.plugin;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

@Configuration
@Component
public class MybatisCryptoConfig {
    @Bean
    public ReadInterceptor readInterceptorPlugin(){
        return new ReadInterceptor();
    }

    @Bean
    public WriteInterceptor writeInterceptorPlugin(){
        return new WriteInterceptor();
    }
    
}    

测试数据插入和查询的效果

import javax.annotation.Resource;
@Slf4j
@SpringBootTest
public class MyBatisTest {
    @Resource
    private AccountMapper accountMapper;
    
    @Test
    void test81901(){
        Account build = Account.builder()
                .username("htgy4")
                .passwordCiper("tnmt3t")
                .build();
        accountMapper.insertOne(build);
        //insert插入时,自动加密
        System.err.println(build);
    }

    @Test
    void test81902(){
        //select查询时,自动解密
        System.out.println(accountMapper.findOneById(14));
        System.err.println(accountMapper.selectUserList(Account.builder()
                .id(15)
                .build()));
    }
    
}    
--insret插入操作日志
2023-06-19 19:50:06.800  INFO 10344 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2023-06-19 19:50:07.165  INFO 10344 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@1878045132 wrapping com.mysql.cj.jdbc.ConnectionImpl@3b55dd15] will not be managed by Spring
==>  Preparing: insert into t_account(username,password_ciper) values (?,?)
==> Parameters: hrwgy4(String),  b72c2ec22d87f504(String)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@33feb805]
Account(id=16, username=hrwgy4,  passwordCiper=b72c2ec22d87f504)


--select查询操作日志
Account(id=14, username=t43t34t,  passwordCiper=aret3t)
[Account(id=15, username=htgy4,  passwordCiper=tnmt3t)]