MyBatis插件实现字段加解密实践

背景

互联网系统充斥着各种敏感信息,包括各种个人信息、商业信息等等。按照要求,不允许隐私信息明文存储,需要进行加密处理,防止造成隐私泄露的风险。

我司作为一个跨境电商公司,各系统中,自然免不了涉及各类敏感信息,并且对各类敏感信息的安全级别进行了划分,不同等级的加密要求级别有一定的差别。

方案

由于不同的敏感数据需要使用不同的加解密策略,在MySQL层面,无法满足需求,所以只能在应用代码层面进行实现。但需要考虑几个点

  • 尽可能减少对业务代码侵入性;
  • 以最小的风险进行改动;
  • 方案可复用,方便拓展;

手动加解密

这是首先被提出来的方案。该方案做法是

  • 在任何涉及到读取敏感字段的业务代码中,进行手动解密操作。
  • 在任何涉及到写入敏感字段的业务代码中,进行手动加密操作。

优点:

  • 暂无;

缺点:

  • 侵入业务代码,业务开发甚至需要关注不同级别的加解密策略;
  • 老旧系统调用复杂,可能出现重复加密或重复解密,导致无法复原原始数据,风险数据非常高;
  • 若数据安全级别变动,加密策略升级,所有可能涉及到的业务代码都需要变更,风险半径无法预测;

综上所述,该方案完全无法满足我们对方案的要求,属于最笨的方案,可以直接say no。

自动加解密

由于无法在MySQL层面实现,又希望尽可能减少对业务代码的侵入性,那么任务只能落在ORM框架或半ORM框架上。我们系统使用的是MyBatis,那么利用MyBatis的插件机制来实现自动加解密,是个不错的选择。

优点:

  • 业务代码无需改造,几乎对业务代码无入侵性;
  • 加解密统一入口,风险可控;
  • 方案多个系统通用,加解密策略可随时拓展;

缺点:

  • 暂无;

实现

编码

由于敏感数据被划分成多个不同级别,各个级别使用的加解密算法不同,所以面对这种不同加密算法的场景,策略模式非常适合;

加解密策略

arduino 复制代码
public interface SensitiveStrategy {

    /**
     * 加密
     */
    String encrypt(String value);

    /**
     * 解密
     */
    String decrypt(String value);
}

各种加解密算法,只要实现该接口即可,此处略。

自定义注解

自定义一个字段上的注解,目的是为了让MyBatis拦截器识别哪些字段需要加解密,加解密的策略是什么。

less 复制代码
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
public @interface SensitiveField {

    Class<? extends SensitiveStrategy> sensitiveStrategy();
}

MyBatis拦截器

我们需要定义一个MyBatis拦截器,该拦截器的作用有以下:

  • 拦截入参,识别被@SensitiveField注解的字段,并使用指定加密策略,对字段内容进行加密;
  • 拦截查询结果,识别被@SensitiveField注解的字段,并使用指定的解密策略,对字段内容进行解密;

我们知道,MyBatis的拦截器插件,可以对四大组件Executor、StatementHandler、ParameterHandler、ResultSetHandler进行拦截,由于我们只需要对入参和结果进行拦截和修改,所以只需指定拦截ParameterHandler、ResultSetHandler即可。

less 复制代码
@Slf4j
@Component
@Intercepts(value = {
        @Signature(type = ParameterHandler.class,method = "setParameters",args = {PreparedStatement.class}),
        @Signature(type = ResultSetHandler.class,method = "handleResultSets",args = {Statement.class})
})
public class SensitiveInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        Object target = invocation.getTarget();
        //入参加密
        if (target instanceof ParameterHandler parameterHandler){
            //获取参数对象
            Object parameterObj = ReflectUtil.getFieldValue(parameterHandler, "parameterObject");
            if (parameterObj != null){
                //获取参数对象内的字段
                Arrays.stream(ReflectUtil.getFields(parameterObj.getClass()))
                        .filter(field -> String.class.equals(field.getType()))
                        .filter(field -> field.getAnnotation(SensitiveField.class)!=null )
                        .filter(field -> ReflectUtil.getFieldValue(parameterObj,field) != null)
                        .forEach(field -> {
                            SensitiveField sensitiveField = field.getAnnotation(SensitiveField.class);
                            Class<? extends SensitiveStrategy> strategyClazz = sensitiveField.sensitiveStrategy();
                            if (strategyClazz != null){
                                SensitiveStrategy strategy = SpringContext.getBean(strategyClazz);
                                String encrypt = strategy.encrypt(ReflectUtil.getFieldValue(parameterObj, field).toString());
                                ReflectUtil.setFieldValue(parameterObj,field,encrypt);
                            }
                        });
                ReflectUtil.setFieldValue(parameterHandler,"parameterObject",parameterObj);
            }
        }

        Object resultObj = invocation.proceed();

        //出参解密
        if (resultObj != null && target instanceof ResultSetHandler){
            List<?> resultList = (List<?>) resultObj;
            for (Object result : resultList) {
                if (!SimpleTypeRegistry.isSimpleType(result.getClass())){
                    Arrays.stream(ReflectUtil.getFields(result.getClass()))
                            .filter(field -> String.class.equals(field.getType()))
                            .filter(field -> field.getAnnotation(SensitiveField.class)!=null )
                            .filter(field -> ReflectUtil.getFieldValue(result,field) != null)
                            .forEach(field -> {
                                SensitiveField sensitiveField = field.getAnnotation(SensitiveField.class);
                                Class<? extends SensitiveStrategy> strategyClazz = sensitiveField.sensitiveStrategy();
                                if (strategyClazz != null){
                                    SensitiveStrategy strategy = SpringContext.getBean(strategyClazz);
                                    String decrypt = strategy.decrypt(ReflectUtil.getFieldValue(result, field).toString());
                                    ReflectUtil.setFieldValue(result,field,decrypt);
                                }
                            });
                }
            }
            resultObj = resultList;
        }
        return resultObj;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target,this);
    }
}

由于系统是基于SpringBoot的,所以我们将所有实现的加解密策略对象,交给Spring容器管理。在MyBatis拦截器中,直接从Spring容器中获取对应加解密策略使用接口。

上线

上线阶段,我们分为以下几个步骤实施

  • 临时关闭敏感数据列的修改功能入口,避免出现备份后源数据变更;
  • 备份敏感字段列数据,小表DBA直接备份,大表编写代码分批备份;
  • 利用apollo控制读取时不解密,写入时加密,对对应字段进行一次读取后更新即可完成加密;
  • 观察涉及的业务功能读取是否正常;
  • 开启敏感数据修改功能入口,观察系统是否正常;

其他问题

其实在整个过程中,实现功能相关的编码并不复杂。除此之外,需要去识别并解决其他的一些问题,这期间花费了更多的时间,例如

敏感字段like搜索

字段加密后存储,就无法直接使用like关键字搜索。其实经过我司安全部门评估,敏感字段也不允许进行模糊搜索,有数据泄露风险,所以在产品方案层面,直接砍掉了类似功能;

若是一定要保留改功能,也可以使用以下方案

  • 拓展一个新字段,用于模糊搜索,类型为text;
  • 对原始字符进行分割,可按每N个字符为一项进行分割,分割后每一项使用固定加密算法加密,再使用固定字符对每一项进行拼接,形成新的字符串,保存到新字段;
  • like查询时,同样也是分割、加密、拼接的方式;

注意,like字段需要使用text类型,性能会很低。

敏感字段group by

对于一些敏感级别较低的字段,采用了固定加密方式(即多次对相同的数据进行加密后结果不变),此时由于结果不变,可以直接使用加密后的字段进行group by;

但是对于敏感级别较高的字段,我司采用了动态加密方式(即多次对相同的数据加密结果不一致),此时由于结果不一致,无法进行group by操作。解决方案的拓展多一列,对源数据使用固定加密后,将结果存进该拓展字段,group by业务使用。

相关推荐
魔道不误砍柴功1 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2341 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨1 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
Chrikk3 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*3 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue3 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man3 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
测开小菜鸟3 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity4 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天4 小时前
java的threadlocal为何内存泄漏
java