MyBatis实现字段加解密的实践

 更新时间:2023年11月14日 10:40:27   作者:十年培训经验的菜包  
为了数据安全问题,有时候需要将部分敏感字段加密后再入库,本文主要介绍了MyBatis实现字段加解密的实践,具有一定的参考价值,感兴趣的可以了解一下

背景

互联网系统充斥着各种敏感信息,包括各种个人信息、商业信息等等。按照要求,不允许隐私信息明文存储,需要进行加密处理,防止造成隐私泄露的风险。
我司作为一个跨境电商公司,各系统中,自然免不了涉及各类敏感信息,并且对各类敏感信息的安全级别进行了划分,不同等级的加密要求级别有一定的差别。

方案

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

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

手动加解密

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

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

优点:

  • 暂无;

缺点:

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

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

自动加解密

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

优点:

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

缺点:

  • 暂无;

实现

编码

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

加解密策略

public interface SensitiveStrategy {

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

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

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

自定义注解

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

@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即可。

@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业务使用。

到此这篇关于MyBatis实现字段加解密的实践的文章就介绍到这了,更多相关MyBatis 字段加解密内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java多线程中sleep和wait区别

    Java多线程中sleep和wait区别

    本文主要介绍了Java多线程中sleep和wait区别,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-06-06
  • Spring整合junit的配置过程图解

    Spring整合junit的配置过程图解

    这篇文章主要介绍了Spring整合junit的配置过程图解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-02-02
  • Java并发CopyOnWrite容器原理解析

    Java并发CopyOnWrite容器原理解析

    这篇文章主要介绍了Java并发CopyOnWrite容器原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-01-01
  • Java中使用Spring Retry实现重试机制的流程步骤

    Java中使用Spring Retry实现重试机制的流程步骤

    这篇文章主要介绍了我们将探讨如何在Java中使用Spring Retry来实现重试机制,重试机制在处理临时性故障和提高系统稳定性方面非常有用,文中通过代码示例介绍的非常详细,具有一定的参考价值,需要的朋友可以参考下
    2024-07-07
  • java将m3u8格式转成视频文件的方法

    java将m3u8格式转成视频文件的方法

    这篇文章主要介绍了如何java将m3u8格式转成视频文件,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-03-03
  • Java 判断字符串中是否包含中文的实例详解

    Java 判断字符串中是否包含中文的实例详解

    这篇文章主要介绍了Java 判断字符串中是否包含中文的实例详解的相关资料,这里提供实例来说明该如何实现这样的功能,需要的朋友可以参考下
    2017-08-08
  • 结合Mybatis聊聊对SQL注入的见解

    结合Mybatis聊聊对SQL注入的见解

    这篇文章主要介绍了结合Mybatis聊聊对SQL注入的见解,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-12-12
  • Java微信公众号推送模版消息的步骤示例详解

    Java微信公众号推送模版消息的步骤示例详解

    模板消息是一种向用户发送通知的服务,广泛用于订单状态更新、服务提醒等场景,下面,我将详细介绍如何使用Java结合微信官方提供的API来实现模板消息的推送,感兴趣的朋友跟随小编一起看看吧
    2024-08-08
  • Spring Boot 利用注解方式整合 MyBatis

    Spring Boot 利用注解方式整合 MyBatis

    这篇文章主要介绍了Spring Boot 利用注解方式整合 MyBatis,文章围绕主主题的相关资料展开详细的内容介绍,需要的小伙伴可以参考一下
    2022-05-05
  • java实现桌球游戏

    java实现桌球游戏

    这篇文章主要为大家详细介绍了java实现桌球游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-10-10

最新评论