谈谈敏感字段加密处理

1,774 阅读4分钟

这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战

背景

​ 在一些比较敏感的应用场景下,可能存在对数据库的部分字段进行加密处理,然而通过什么手段可以良好的进行统一的加密处理,使得业务层调用层可以减少对字段加解密的关注,也就是说如果做到统一抽取加解密的逻辑使得对业务调用层无感?

分析

​ 从mybatis的获取数据方式进行入手,在有了mybatis-plus的加持之后,应该有三种方式进行数据的获取。

  • 通过mybatis-plus扩展的通用接口ServiceImpl,封装了一些通用的增删改查接口。
  • 通过mybatis-plusLambdaQueryWrapper进行手动编写查询条件进行获取数据。
  • 通过编写具体是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设置参数的环节,然后对参数的进行对应的加解密处理。(这里只展示参数处理的加密逻辑,没有展示结果集的解密逻辑)

  1. 获取参数对像,即 mapperparamsType的实例
  2. xml编写sql脚本的方式或exampleupdate操作
  3. 获取不到exampleupdate操作参数信息, 则表示为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方式传参,二者都不能有效的进行处理,只能通过业务端进行一些约定,通过实体的方式进行传参。