这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战
背景
在一些比较敏感的应用场景下,可能存在对数据库的部分字段进行加密处理,然而通过什么手段可以良好的进行统一的加密处理,使得业务层调用层可以减少对字段加解密的关注,也就是说如果做到统一抽取加解密的逻辑使得对业务调用层无感?
分析
从mybatis
的获取数据方式进行入手,在有了mybatis-plus
的加持之后,应该有三种方式进行数据的获取。
- 通过
mybatis-plus
扩展的通用接口ServiceImpl
,封装了一些通用的增删改查接口。 - 通过
mybatis-plus
的LambdaQueryWrapper
进行手动编写查询条件进行获取数据。 - 通过编写具体是
SQL
脚本到xxx.xml
文件中,进行数据的获取。
所以,若需要使得业务端对某些字段的加解密无感,则需要抽取的结构要竟可能的涵盖上述三种方式。
不论通过什么方式进行数据库字段的加解密统一处理,首先都是要定义一个注解,用于标记一个实体中那些字段需要进行加解密处理。这里暂时对使用地方进行屏蔽,具体的标记点需结合对应的处理手段。
//@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataEncrypt {
String key() default "";
}
复制代码
方式一
最为简单粗暴的方式就是通过AOP
,直接将mapper
包下的所有xxxMapper
文件都进行拦截,然后通过反射对操作实体进行属性遍历,获取对应的标记注解,来判断是否进行加密,并且在返回数据后,同样进行使用反射对响应结果列表进行数据解密。
@Slf4j
@Aspect
@Component
public class JpaDataEncryptAspect {
@Value("${key:ABCDEFGHIJKLMN}")
private String defaultKey;
/**
* 切点
*/
@Pointcut("execution(public * com.cn.xiaocainiaoya..mapper..*Mapper.*(..))")
public void pointCut() {
}
@Around("pointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
// 执行前参数加密
for (Object arg : point.getArgs()) {
dataEncrypt(arg);
}
//执行方法
Object result = point.proceed();
// 执行完参数解密
for (Object arg : point.getArgs()) {
dataDesEncrypt(arg);
}
// 执行完结果解密
if (result instanceof List) {
List<Object> tmps = new ArrayList<>();
List list = ((List) result);
Iterator it = list.iterator();
while (it.hasNext()) {
Object obj = it.next();
// 先移除
it.remove();
tmps.add(dataDesEncrypt(obj));
}
list.addAll(tmps);
return list;
} else if (result instanceof IPage) {
List<Object> tmps = new ArrayList<>();
List list = ((IPage) result).getRecords();
Iterator it = list.iterator();
while (it.hasNext()) {
Object obj = it.next();
// 先移除
it.remove();
tmps.add(dataDesEncrypt(obj));
}
list.addAll(tmps);
((IPage) result).setRecords(list);
return result;
} else {
return dataDesEncrypt(result);
}
}
}
复制代码
**注意:**处理上有两点需要注意,1.由于参数进行数据的获取处理之后,可能业务层还需使用,所以在进行具体的目标方法的执行之后,需要将数据解密为原样。2.对应的列表形式List
数据,也需要进行处理。
方式二
通过mybatis
的开放扩展接口,编写对应的插件进行拦截,统一进行数据加解密的处理。通过插件方式处理,需要对mybatis
的一些内部结构有所了解,这里拦截了mybatis
设置参数的环节,然后对参数的进行对应的加解密处理。(这里只展示参数处理的加密逻辑,没有展示结果集的解密逻辑)
@Intercepts({@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),})
@Component
@Slf4j
public class ParameterInterceptor implements Interceptor {
private final static String RECORD = "record";
private final static String EXAMPLE = "example";
/**
* 拦截器处理接口
*
* @Author: xiaocainiaoya
* @Date: 2020/6/11
* @param invocation
* @return: java.lang.Object
**/
@Override
public Object intercept(Invocation invocation) throws Throwable {
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
// 获取参数对像,即 mapper 中 paramsType 的实例
Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
parameterField.setAccessible(true);
Object parameterObject = parameterField.get(parameterHandler);
Field boundSqlField = parameterHandler.getClass().getDeclaredField("boundSql");
boundSqlField.setAccessible(true);
BoundSql boundSql = (BoundSql) boundSqlField.get(parameterHandler);
Field mappedStatement = parameterHandler.getClass().getDeclaredField("mappedStatement");
mappedStatement.setAccessible(true);
//MappedStatement ms = (MappedStatement)mappedStatement.get(parameterHandler);
// xml编写sql脚本的方式或example的update操作
if(parameterObject instanceof MapperMethod.ParamMap){
MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap)parameterObject;
// 使用example的update操作方式
Object updateEntity = null;
Object example = null;
try{
updateEntity = paramMap.get(RECORD);
example = paramMap.get(EXAMPLE);
}catch(Exception e){
// 获取不到example的update操作参数信息, 则表示为xml编写sql方式, 直接执行目标方法
return invocation.proceed();
}
if(ObjectUtil.isNotNull(updateEntity) && ObjectUtil.isNotNull(example)){
// 修改的实体
Field[] fields = ConvertUtils.getAllFields(updateEntity);
for(Field field : fields){
JpaDataEncrypt jpaDataEncrypt = field.getAnnotation(JpaDataEncrypt.class);
if(ObjectUtil.isNull(jpaDataEncrypt)){
continue;
}
field.setAccessible(true);
Object value = field.get(updateEntity);
if(!(value instanceof String) || StringUtils.isEmpty((String)value)){
continue;
}
field.set(updateEntity, Sm4DecUtils.encrypt(strValue));
}
// 修改条件example
encryptExampleInfo((Example) example, boundSql);
}
}else if(parameterObject instanceof Example){
// 使用example方式查询的情况
encryptExampleInfo((Example)parameterObject, boundSql);
}else{
// 实体查询 无法判断类型
}
return invocation.proceed();
}
@Override
public void setProperties(Properties properties) {
}
}
复制代码
对于比较单一的查询元素或者是通过Map
方式传参给xml
问文件的方式,目前来看这种插件的方式好像不好处理,因为这里获取到的类型也是map
类型,无法获取到那个字段进行了注解的标记。所以这种方式只能由业务端进行数据加密,这可能导致抽取的不纯粹。
@Mapper
public interface UserInfoMapper extends CommonMapper<UserInfo> {
List<UserInfo> queryUserInfo(@Param("queryParam") Map queryParam);
}
复制代码
总结
对比以上两种方式:
方式一需要为mapper
下所有包做动态代理,编写上比较简单,但是它无法满足LambdaQueryWrapper
方式的查询,也就是说开头提到的三点中,方式一不满足第二点。并且它也不满足如果第三点中如果是map
方式传参的场景。
方式二通过mybatis
开放的扩展机制,需要编写对应的参数设置拦截和结果集拦截,但是它无法满足开头中提到的第三点。
所以以上两种方式各有利弊,都不能完全处理掉所有的场景,其实对于第三点中使用map
方式传参,二者都不能有效的进行处理,只能通过业务端进行一些约定,通过实体的方式进行传参。