实际的开发中,会有些字段值是不能明文存储在数据库的,例如密码、余额等。本文介绍的是通过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 : >>>>>>User:User(id=5, name=CSDN 彩虹丶, sex=1, age=24, password=111111, phone=15624560000)
查询的时候也会进行拦截,把输入的参数加密后再去数据库查询,查询出来的数据经过解密后,可以看到password和输入时的是一致的。
散会!