谈谈敏感字段加密处理

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

背景

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

分析

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

  • 通过mybatis-plus扩展的通用接口ServiceImpl,封装了一些通用的增删改查接口。
  • 通过mybatis-plusLambdaQueryWrapper进行手动编写查询条件进行获取数据。
  • 通过编写具体是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方式传参,二者都不能有效的进行处理,只能通过业务端进行一些约定,通过实体的方式进行传参。

おすすめ

転載: juejin.im/post/7062224269626834951