背景
我们把数据存到数据库的时候,有些敏感字段是需要加密的,从数据库查出来再进行解密。如果存在多张表或者多个地方需要对部分字段进行加解密操作,每个地方都手写一次加解密的动作,显然不是最好的选择。如果我们使用的是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自定义注解加解密的资料请关注代码网其它相关文章!
发表评论