背景
我们把数据存到数据库的时候,有些敏感字段是需要加密的,从数据库查出来再进行解密。如果存在多张表或者多个地方需要对部分字段进行加解密操作,每个地方都手写一次加解密的动作,显然不是最好的选择。如果我们使用的是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 - 并调用这个
bean的addinnerinterceptor方法,把所有的innerinterceptor加入进去,才能生效 innerinterceptor只有before拦截,缺省after拦截。加密可以在before里面完成,但解密需要在after里面完成,所以这个innerinterceptor不能满足我们的要求
所以继续研究源码,发现mybatis有个org.apache.ibatis.plugin.interceptor接口,这个接口能满足我对自动加解密的所有诉求
- 首先,实现
interceptor接口,只要注册成为spring容器的bean,拦截器就能生效 - 可以更加灵活的在
before和after之间插入自己的逻辑
加密拦截器
创建名为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());
}
}
单元测试运行截图

以上就是mybatis-plus根据自定义注解实现自动加解密的示例代码的详细内容,更多关于mybatis-plus自定义注解加解密的资料请关注代码网其它相关文章!
发表评论