一、背景
我司最近在做等保,要求数据库里面的手机号、微信号、身份证号等等这些在数据库中都不能是明文,所以需要把数据库中的涉及到这些的加密保存,但是查询和保存的接口传参不受影响,之前业务给前端返回是什么样子依然保持不变,这样前端不受影响。
二、实现方式的思考
1.可以直接代码修改,代码中涉及的敏感数据接口在查询和插入、更新时进行加解密。缺点是工作量大,代码侵入多。
2.在mybatis中进行统一拦截,上层业务调用不需要再考虑敏感数据的加解密问题,可以考虑配置文件中配置需要加解密的表和字段,或者注解的方式。
也看了一些博客,最终采用了第二种方式,通过拦截器+注解的方式,实现敏感数据自动加解密
三、mybatis插件的原理:
这里也可以自行查询,资料也很多。
Mybatis的插件,是采用责任链机制,通过JDK动态代理来实现的。默认情况下,Mybatis允许使用插件来拦截四个对象:
Executor:执行CURD操作;
StatementHandler:处理sql语句预编译,设置参数等相关工作;
ParameterHandler:设置预编译参数用的;
ResultSetHandler:处理结果集。
四、代码实现
设置参数时对参数中含有敏感字段的数据进行加密
对查询返回的结果进行解密处理
按照对Executor和ResultSetHandler
进行切入,这里
也参考了一些博主,大部分是按照对ParameterHandler
和ResultSetHandler
进行切入,但在实践中发现ParameterHandler
在某些场景支持(比如传参对象中有字段为list或者mapper中是list传入的)的不好,但我司项目中是有很多这种情况的。后面通过跟源码发现是在改值之前分页插件已经赋值了查询的参数,所以后面对list改值之后并未生效。
实体类中有加解密字段时使用@SensitiveData注解,单个需加解密的字段使用@SensitiveField注解
注解:
SensitiveData
注解:用在实体类上,表示此类有些字段需要加密,需要结合@SensitiveField
一起使用SensitiveField
注解:用在类的字段上或者方法的参数上,表示该字段或参数需要加密
1.SensitiveData注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created on 2024/1/9
* * 该注解定义在类上
* * 插件通过扫描类对象是否包含这个注解来决定是否继续扫描其中的字段注解
* * 这个注解要配合SensitiveField注解
* @author www
* @version V1.0
* @apiNote
*/
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
}
2.SensitiveField注解
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created on 2024/1/9
* 该注解有两种使用方式
* ①:配合@SensitiveData加在类中的字段上
* ②:直接在Mapper中的方法参数上使用
* @author www
* @version V1.0
* @apiNote
*/
@Documented
@Inherited
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveField {
}
插件实现代码:
1.入参处理
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
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.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
}
)
@Component
public class BarExecuteInterceptor implements Interceptor {
private static final Logger log = LoggerFactory.getLogger(BarExecuteInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
if (invocation.getTarget() instanceof Executor) {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Object parameterObject = invocation.getArgs()[1];
if (parameterObject != null) {
//批量保存
if (parameterObject instanceof Map) {
Map map = (Map) parameterObject;
for (Object key : map.keySet()) {
Object value = map.get(key);
if (value != null){
if (value instanceof List) {
List list = (List) value;
for (Object item : list) {
//list<phone>
if (item instanceof String) {
if (isMethodParameterAnnotated(mappedStatement, (String) key)) {
String encryptedItem = EncryptUtil.encryptValue((String) item);
//当list中只有一个值时可能是singletonList,
if (list != null && list.size() == 1){
list = new ArrayList<>(list);
}
// 替换原有值
list.set(list.indexOf(item), encryptedItem);
}
} else {
//OBJECT 类型 走这个方法
// 递归处理其他类型的对象
processSensitiveFields(mappedStatement, item);
}
}
map.put(key,list);
} else if (value instanceof String) {
if (isMethodParameterAnnotated(mappedStatement, (String) key)) {
String encryptedItem = EncryptUtil.encryptValue((String) value);
map.put(key, encryptedItem);
}
} else if (value instanceof Map) {
// Map入参情况,这种不用处理,实际map的也没办法加加密的注解
continue;
} else {
processSensitiveFields(mappedStatement, value);
}
}
}
} else if (parameterObject instanceof List) {
// 检查是否为TestForm实例或者包含TestForm实例的列表
// 如果参数是列表,检查列表中的每个元素是否为TestForm实例
List<?> parameterList = (List<?>) parameterObject;
for (Object param : parameterList) {
processSensitiveFields(mappedStatement, param);
}
} else if (parameterObject instanceof String) {
if (isMethodSingleParameterAnnotated(mappedStatement)) {
String encryptedItem = EncryptUtil.encryptValue((String) parameterObject);
invocation.getArgs()[1] = encryptedItem;
}
} else {
//通用的保存单条数据时,对象查询
processSensitiveFields(mappedStatement, parameterObject);
}
}
// 继续执行原始方法
return invocation.proceed();
}
} catch (Exception e) {
log.error("加密参数处理发生异常:{}",e.getMessage(),e);
}
// 如果不是Executor&#x