这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战
背景
在一些比较敏感的应用场景下,可能存在对数据库的部分字段进行加密处理,然而通过什么手段可以良好的进行统一的加密处理,使得业务层调用层可以减少对字段加解密的关注,也就是说如果做到统一抽取加解密的逻辑使得对业务调用层无感?
分析
从mybatis
的获取数据方式进行入手,在有了mybatis-plus
的加持之后,应该有三种方式进行数据的获取。
- 通过
mybatis-plus
扩展的通用接口ServiceImpl
,封装了一些通用的增删改查接口。 - 通过
mybatis-plus
的LambdaQueryWrapper
进行手动编写查询条件进行获取数据。 - 通过编写具体是
SQL
脚本到xxx.xml
文件中,进行数据的获取。
所以,若需要使得业务端对某些字段的加解密无感,则需要抽取的结构要竟可能的涵盖上述三种方式。
不论通过什么方式进行数据库字段的加解密统一处理,首先都是要定义一个注解,用于标记一个实体中那些字段需要进行加解密处理。这里暂时对使用地方进行屏蔽,具体的标记点需结合对应的处理手段。
@Retention(RetentionPolicy.RUNTIME)
public @interface DataEncrypt {
String key() default "";
}
方式一
最为简单粗暴的方式就是通过AOP
,直接将mapper
包下的所有xxxMapper
文件都进行拦截,然后通过反射对操作实体进行属性遍历,获取对应的标记注解,来判断是否进行加密,并且在返回数据后,同样进行使用反射对响应结果列表进行数据解密。
@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);
}
// ... 执行完结果解密
}
}
注意:处理上有两点需要注意,1.由于参数进行数据的获取处理之后,可能业务层还需使用,所以在进行具体的目标方法的执行之后,需要将数据解密为原样。2.对应的列表形式List
数据,也需要进行处理。
方式二
通过mybatis
的开放扩展接口,编写对应的插件进行拦截,统一进行数据加解密的处理。通过插件方式处理,需要对mybatis
的一些内部结构有所了解,这里拦截了mybatis
设置参数的环节,然后对参数的进行对应的加解密处理。(这里只展示参数处理的加密逻辑,没有展示结果集的解密逻辑)
- 获取参数对像,即
mapper
中paramsType
的实例 xml
编写sql
脚本的方式或example
的update
操作- 获取不到
example
的update
操作参数信息, 则表示为xml
编写sql
方式, 直接执行目标方法
@Intercepts({@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class)})
@Component
public class ParameterInterceptor implements Interceptor {
@Override
public Object intercept(Invocation i) throws Throwable {
ParameterHandler handler = (ParameterHandler) invocation.getTarget();
// 1.
Field paramField = handler.getClass().getDeclaredField("paramObject");
// ... 使用反射获取handler相关对象
// 2.
if(paramObject instanceof MapperMethod.ParamMap){
MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap)paramObject;
// 使用example的update操作方式
Object updateEntity = null;
Object example = null;
try{
updateEntity = paramMap.get(RECORD);
example = paramMap.get(EXAMPLE);
}catch(Exception e){
// 3.
return invocation.proceed();
}
if(ObjectUtil.isNotNull(example)){
Field[] fields = ConvertUtils.getAllFields(updateEntity);
// ...实体加密逻辑
encryptExampleInfo(example, boundSql);
}
}else if(paraObject instanceof Example){
// 使用example方式查询的情况
encryptExampleInfo(paramObject, boundSql);
}else{
// 实体查询 无法判断类型
}
return invocation.proceed();
}
}
对于比较单一的查询元素或者是通过Map
方式传参给xml
问文件的方式,目前来看这种插件的方式好像不好处理,因为这里获取到的类型也是map
类型,无法获取到那个字段进行了注解的标记。所以这种方式只能由业务端进行数据加密,这可能导致抽取的不纯粹。
@Mapper
public interface UserInfoMapper extends CommonMapper<UserInfo> {
List<UserInfo> queryUserInfo(@Param("queryParam") Map queryParam);
}
总结
对比以上两种方式:
方式一需要为mapper
下所有包做动态代理,编写上比较简单,但是它无法满足LambdaQueryWrapper
方式的查询,也就是说开头提到的三点中,方式一不满足第二点。并且它也不满足如果第三点中如果是map
方式传参的场景。
方式二通过mybatis
开放的扩展机制,需要编写对应的参数设置拦截和结果集拦截,但是它无法满足开头中提到的第三点。
所以以上两种方式各有利弊,都不能完全处理掉所有的场景,其实对于第三点中使用map
方式传参,二者都不能有效的进行处理,只能通过业务端进行一些约定,通过实体的方式进行传参。