前言
通过mybatis提供的拦截器,在新增、修改时对特定的敏感字段进行加密存储,查询时自动进行解密操作,减少业务层面的代码逻辑;
加密存储意义:
- 防止数据泄露:即使数据库被非法访问或泄露,加密数据也无法被直接利用
- 保护个人隐私:如身份证号、手机号、住址等pii(个人身份信息)数据
- 保障财务安全:加密银行卡号、支付密码等金融信息
核心逻辑:
- 自定义注解,对需要进行加密存储的使用注解进行标注;
- 构建aes对称加密工具类;
- 实现mybatis拦截器,通过反射获取当前实体类的字段是否需要进行加解密;
实现
自定义注解
通过自定义@encryptdbbean与@encryptdbcolumn标识某个do实体类的某些字段需要进行加解密处理;
- encryptdbbean:作用在类上
- encryptdbcolumn:作用在字段上
@inherited
@target({elementtype.type})
@retention(retentionpolicy.runtime)
public @interface encryptdbbean {
}
@retention(retentionpolicy.runtime)
@target(elementtype.field)
public @interface encryptdbcolumn {
}
aes对称加密工具类
import javax.crypto.cipher;
import javax.crypto.spec.ivparameterspec;
import javax.crypto.spec.secretkeyspec;
import java.util.base64;
public class dbaesutils {
/**
* 设置为cbc加密模式,默认情况下ecb比cbc更高效
*/
private final static string cbc = "/cbc/pkcs5padding";
private final static string algorithm = "aes";
/**
* 定义密钥key,aes加密算法,key的大小必须是16个字节
*/
private final static string key = "1234567812345678";
/**
* 设置偏移量,iv值任意16个字节
*/
private final static string iv = "1122334455667788";
/**
* 对称加密数据
*
* @return : 密文
* @throws exception
*/
public static string encryptbysymmetry(string input) {
try {
// cbc模式
string transformation = algorithm + cbc;
// 获取加密对象
cipher cipher = cipher.getinstance(transformation);
// 创建加密规则
// 第一个参数key的字节
// 第二个参数表示加密算法
secretkeyspec sks = new secretkeyspec(key.getbytes(), algorithm);
// encrypt_mode:加密模式
// decrypt_mode: 解密模式
// 使用cbc模式
ivparameterspec iv = new ivparameterspec(iv.getbytes());
cipher.init(cipher.encrypt_mode, sks, iv);
// 加密
byte[] bytes = cipher.dofinal(input.getbytes());
// 输出加密后的数据
return base64.getencoder().encodetostring(bytes);
} catch (exception e) {
throw new runtimeexception("加密失败!", e);
}
}
/**
* 对称解密
*
* @param input : 密文
* @throws exception
* @return: 原文
*/
public static string decryptbysymmetry(string input) {
try {
// cbc模式
string transformation = algorithm + cbc;
// 1,获取cipher对象
cipher cipher = cipher.getinstance(transformation);
// 指定密钥规则
secretkeyspec sks = new secretkeyspec(key.getbytes(), algorithm);
// 使用cbc模式
ivparameterspec iv = new ivparameterspec(iv.getbytes());
cipher.init(cipher.decrypt_mode, sks, iv);
// 3. 解密,上面使用的base64编码,下面直接用密文
byte[] bytes = cipher.dofinal(base64.getdecoder().decode(input));
// 因为是明文,所以直接返回
return new string(bytes);
} catch (exception e) {
throw new runtimeexception("解密失败!", e);
}
}
}
创建拦截器
- 加密拦截器:encryptinterceptor
- 解密拦截器:decryptinterceptor
加密拦截器
在新增或者更新时,通过拦截对被注解标识的字段进行加密存储处理;
import com.lhz.demo.annotation.encryptdbbean;
import com.lhz.demo.annotation.encryptdbcolumn;
import com.lhz.demo.utils.dbaesutils;
import lombok.extern.slf4j.slf4j;
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.*;
@slf4j
@component
@intercepts({
@signature(type = parameterhandler.class, method = "setparameters", args = {preparedstatement.class}),
})
public class encryptinterceptor implements interceptor {
@override
public object intercept(invocation invocation) throws throwable {
try {
parameterhandler parameterhandler = (parameterhandler) invocation.gettarget();
field parameterfield = parameterhandler.getclass().getdeclaredfield("parameterobject");
parameterfield.setaccessible(true);
object parameterobject = parameterfield.get(parameterhandler);
if (parameterobject != null) {
set<object> objectlist = new hashset<>();
if (parameterobject instanceof map<?, ?>) {
collection<?> values = ((map<?, ?>) parameterobject).values();
objectlist.addall(values);
} else {
objectlist.add(parameterobject);
}
for (object o1 : objectlist) {
class<?> o1class = o1.getclass();
// 实体类是否存在 加密注解
boolean encryptdbbean = o1class.isannotationpresent(encryptdbbean.class);
if (encryptdbbean) {
//取出当前当前类所有字段,传入加密方法
field[] declaredfields = o1class.getdeclaredfields();
// 便利字段,是否存在加密注解,并且进行加密处理
for (field field : declaredfields) {
//取出所有被encryptdecryptfield注解的字段
boolean annotationpresent = field.isannotationpresent(encryptdbcolumn.class);
if (annotationpresent) {
field.setaccessible(true);
object object = field.get(o1);
if (object != null) {
string value = object.tostring();
//加密 这里我使用自定义的aes加密工具
field.set(o1, dbaesutils.encryptbysymmetry(value));
}
}
}
}
}
}
return invocation.proceed();
} catch (exception e) {
throw new runtimeexception("字段加密失败!", e);
}
}
/**
* 默认配置,否则当前拦截器不会加入拦截器链
*/
@override
public object plugin(object o) {
return plugin.wrap(o, this);
}
}
解密拦截器
将查询的数据,返回为do实体类时,对被注解标识的字段进行解密处理
import com.lhz.demo.annotation.encryptdbbean;
import com.lhz.demo.annotation.encryptdbcolumn;
import com.lhz.demo.utils.dbaesutils;
import lombok.extern.slf4j.slf4j;
import org.apache.ibatis.executor.resultset.resultsethandler;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.component;
import org.springframework.util.collectionutils;
import java.lang.reflect.field;
import java.sql.statement;
import java.util.arraylist;
import java.util.list;
import java.util.objects;
@intercepts({@signature(type = resultsethandler.class, method = "handleresultsets", args = {statement.class})})
@slf4j
@component
public class decryptinterceptor implements interceptor {
@override
public object intercept(invocation invocation) throws throwable {
object resultobject = invocation.proceed();
try {
if (objects.isnull(resultobject)) {
return null;
}
// 查询列表数据
if (resultobject instanceof arraylist) {
list list = (arraylist) resultobject;
if (!collectionutils.isempty(list)) {
for (object result : list) {
class<?> objectclass = result.getclass();
boolean encryptdbbean = objectclass.isannotationpresent(encryptdbbean.class);
if (encryptdbbean) {
// 解密处理
decrypt(result);
}
}
}
} else {
// 查询单个数据
class<?> objectclass = resultobject.getclass();
boolean encryptdbbean = objectclass.isannotationpresent(encryptdbbean.class);
if (encryptdbbean) {
// 解密处理
decrypt(resultobject);
}
}
return resultobject;
} catch (exception e) {
throw new runtimeexception("字段解密失败!", e);
}
}
@override
public object plugin(object o) {
return plugin.wrap(o, this);
}
public <t> void decrypt(t result) throws exception {
//取出resulttype的类
class<?> resultclass = result.getclass();
field[] declaredfields = resultclass.getdeclaredfields();
for (field field : declaredfields) {
boolean annotationpresent = field.isannotationpresent(encryptdbcolumn.class);
if (annotationpresent) {
field.setaccessible(true);
object object = field.get(result);
if (object != null) {
string value = object.tostring();
//对注解的字段进行逐一解密
field.set(result, dbaesutils.decryptbysymmetry(value));
}
}
}
}
}
验证
创建实体类
创建实体类,并且使用加密注解@encryptdbbean、@encryptdbcolumn进行标注,此处以手机号为例;
@data
@tablename("sys_user_info")
@encryptdbbean
public class testentity {
/**
* 用户id
*/
@tableid("id")
private long id;
/**
* 用户名称
*/
private string name;
/**
* 手机号
*/
@encryptdbcolumn
private string mobile;
}
数据写入与查询
对数据的操作使用伪代码进行表示
testentity entity = new testentity();
entity.setid(1l);
entity.setname("测试");
entity.setmobile("166xxxx8888");
// 插入数据
entityservice.insert(entity);
// 更新数据
entity.setmobile("166xxxx7777");
entityservice.updatebyid(entity);
// 列表查询
list<testentity> list = testservice.list();
效果:
- insert和update后的数据,在数据库是加密字符串存储的形式;
- list方法查询的数据,将明文进行显示;
加密字段参与查询
如果是加密字段进行条件查询时,需要自行将查询参数进行加密处理,因为数据库是存储的密文,所以查询时也需要使用密文进行匹配,比如:要查询mobile=111的数据
// 伪代码 // 获取前端传入的查询条件 string mobile = "111" // 手动加密 mobile = dbaesutils.decryptbysymmetry(mobile ); testservice.selectbymobile(mobile);
不生效情况
1、在通过lambdaquerywrapper获取querywrapper方式查询时,拦截器无法获取自定义注解对象,需要手动对查询的字段进行加密,比如:
如果是 通过自定义的xml查询,如果入参有加密注解,那么会自动对字段进行加密处理 testmapper.listtest(testentity)
lambdaquerywrapper<testentity> wrapper = new lambdaquerywrapper<>();
string mobile = test.getmobile();
if (mobile != null) {
// mobile在数据库中加密储存,此处需要手动进行加密
mobile = dbaesutils.encryptbysymmetry(mobile);
}
wrapper.eq(stringutils.isnotblank(test.getmobile()), testentity::getmobile, mobile);
list<testentity> testentities = testmapper.selectlist(wrapper);
2、使用mybatis提供的selectone或者getone方法查询时,无法对响应的数据进行解密,需要手动进行处理,比如:
如果是 通过自定义的xml查询,无论多少条数据都会对数据进行解密,testmapper.selectxmlbyid(long id)
testentity one = testservice.getone(new querywrapper<>(), false); // mobile在数据库中加密储存,此处需要手动进行解密 one.setmobile(dbaesutils.decryptbysymmetry(one.getmobile()));
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论