原生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)]