修改数据库的敏感字段-批量刷加密数据

需求

今天分配到一个任务,修改数据库的敏感字段。

具体就是,以前有些数据库的敏感字段,诸如手机号、邮箱这一类的信息,在数据库中应该不存储或加密存储的,但是之前编写的时候并未加密直接存储在数据库了,现在为了安全需要这些敏感字段进行修改。

方法

修改敏感字段的方法有两种,上面也提到了。

  • 一种是不存储在那张表里,关联用户Id从用户表中去查找相关的敏感信息然后填充到当前对象中;
  • 另一种是对数据先进行加密,然后再存储到数据库中。

这两种办法根据不同的场景使用,如果当前表关联了用户Id就使用第一种,否则使用第二种。

问题

第一种办法在修改中没有遇到问题,问题出在第二个修改方法上。

第二种加密方法是使用的KMS加密:

密钥管理服务(KMS)是一套密钥管理系统, 可以针对云上数据/各端上的加密需求精心设计的密码应用服务,为应用提供符合各种要求的密钥服务及极简应用加解密服务,轻松使用密钥来加密保护敏感的数据资产。
KMS密钥流程如下:

  1. 密钥生成 – 统一管理密钥的生成,一般要求根密钥具备较高的随机性以防止密钥被猜测、应用密钥通过安全的分散算法派生生成。
  2. 密钥存储 – 安全的存储密钥,如使用专用的安全存储设施或采用高强度加密保护,防止密钥的泄露和窃取。
  3. 密钥分发 – 确保密钥从生成、存储环境向使用环境传输的过程中不被泄露。
  4. 密钥注销 – 密钥生命周期完结之后,合理、安全地销毁密钥,并对销毁步骤作进行记录。
  5. 密钥更新 – 通过合理的密钥更替机制,降低密钥长期使用带来的暴露风险。一般要求:根密钥长期有效,具备更替能力;应用密钥定期更新,防止恶意破解;过程密钥一次一密,并通过引入时间戳、流水号等应用数据防止重放攻击。
  6. 密钥备份 – 保证重要密钥的备份恢复机制,在密钥丢失、灾难场景下,能够较快恢复密码服务能力,恢复时间目标(RTO)和恢复点目标(RPO)满足业务方需求。
  7. 密钥应用和密码运算服务 – 在具体的应用场景下,KMS还负责为业务方提供与应用相关的安全接口,如:数据加密封装、隐私数据脱敏、接口签名等。

在项目中KMS加密已经被封装成一个自定义注解了,只需要在需要加密的字段上,添加上加密注解并标注需要加密的类型和是否需要精确查找,就可以自动替换SQL语句,将字段加密后再插入/更新到数据库中,同样查找的时候如果检测到标注了加密注解,会将字段进行解密后返回。

然而接下来就连续出现了几个问题。

个别应用能够加密

一张表同时会被多个项目使用,因此需要在涉及这张表的所有实体类敏感字段上,都添加加密注解。

使用sourcegraph搜索出使用到该表的地方,然后添加注解,测试…问题来了,一共四个应用,只有一个应用能够成功加密。

开始以为是加密类型错了,测试之后发现并不是,然后使用在包里面的加密方法上打断点进行debug,debug发现压根就没进加密方法。

那就不是注解的问题了,这种加密一般都会使用拦截器进行修改,在我排查了一番后,发现那几个项目有点老了,所以没有在Spring整合Mybatis的xml文件中添加加密拦截器,添加完之后可以正常加密。

<bean  id="encryptSelectParamterInterceptor" class="xxx.xxx.xxx"/>
<bean  id="encryptInitInterceptor" class="xxx.xxx.xxx"/>
<bean  id="resultInterceptor" class="xxx.xxx.xxx"/>

字段不需要全部加密

在加密过程中,有一个字段是json字段,里面有一个属性是联系人电话号码,这个属性需要加密,其余都不需要加密。

这种情况下,就不能使用注解进行全部加密,一是json字段较长,全部加密后会超过数据库设置的字段限制,另一个这些属性也不需要全部加密。

这时候就需要进行手动加密解密,在插入以及更新的相关方法中,将敏感属性加密,在查找的相关方法中进行解密。

加密方法:

    public void encodeMobile(ClassA A) {
        // 修改敏感字段,将手机号字段加密存储
        try {
            JSONObject jsonObject = new JSONObject(A.getContent());
            // 这里是获取嵌套字段,若只有一层则可以直接get
            JSONObject B = jsonObject.getJSONObject("arrangeCar");
            if (Objects.nonNull(B) && Objects.nonNull(B.get("phone"))) {
                String mobileEncode = JasyptUtilNew.encryptDataByType(EncryptTypeEnum.MOBILE.getCode(), B.get("phone").toString());
                arrangeCar.set("phone", mobileEncode);
            }
            String content = jsonObject.toString();
            A.setContent(content);
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

解密方法:

    public void decodeMobile(ClassA A) {
        // 还原敏感字段,将content中手机号解密还原
        try {
            JSONObject jsonObject = new JSONObject(A.getContent());
            JSONObject B = jsonObject.getJSONObject("arrangeCar");
            if (Objects.nonNull(B) && Objects.nonNull(B.get("phone"))) {
                String mobileDecode = JasyptUtilNew.decryptDataByType(EncryptTypeEnum.MOBILE.getCode(), B.get("phone").toString());
                B.set("phone", mobileDecode);
            }
            String content = jsonObject.toString();
            A.setContent(content);
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

刷数据加密长度过长

以上加密只是争对以后新增的数据,但是以前的旧数据在数据库中还是明文存在的,所以需要批量将旧数据中敏感字段刷为加密字段。

若是整个字段加密的,可以使用公司提供的阿里云批量刷数据接口进行刷数据,需要向相关部门提供数据库实例表名等信息开通权限,然后填写相关信息发送刷数据请求。

重要,刷数据前记得备份!

刷数据过程中请求报错了,只刷了一小半,提示如下:

Data too long for column 'text_varchar' at row 1

这个错误的意思是说加密后的字段太长了,超过了数据库中原本设定的字段最大长度,比如数据库中的手机号和邮箱原本都是设置的varchar(50),加密后的长度都超过了50位,手机号加密后在64位,邮箱在100位左右,我将其改为了varchar(150),之后刷数据成功。

另外还有个字段也报了这个错误,那是个备注字段,而且数据库中的限定是varchar(1000),备注这种字段不确定性太大,有可能很短有可能很长,加密之后可能就更长了,因此将其设定为Text,TEXT的最大长度为65,535(216 – 1),完全够用。

@Test中线程问题

整个字段加密可以使用公司的接口,如果是自定义加密解密,就像上面的的json字段,需要自己编写接口刷数据。

于是我就在单元测试中编写程序进行刷数据,起初我编写的方法是这样的:

    @Test
    public void brushAll() {
        List<WorkItPushLogDO> workItPushLogDOS = workItPushLogMapper.selectAll();
        for (WorkItPushLogDO item : workItPushLogDOS) {
            workItPushLogManager.encodeMobile(item);
            workItPushLogMapper.updateByPrimaryKeySelective(item);
            System.out.println("加密成功" + item.getId());
        }
    }

没想到执行到一半报错了:

Singleton bean creation not allowed while singletons of this factory are in destruction (Do not request a bean from a BeanFactory in a destroy method implementation!

出现这个错误的原因,就是你的子线程去拿bean,但是你的主线程在调用这个线程后就销毁了,所以bean也会随之销毁,这时候子线程再去获取这个bean就会报这个错误。

为什么会导致这个错误呢?

首先需要明确,在Spring@Test注解标记的单元测试方法时,是在主线程中执行的,而不是在子线程中。

当你运行单元测试时,Spring Boot会创建一个单独的测试执行环境,其中包含一个主线程来执行测试方法。主线程会按照定义的顺序依次执行测试方法。

所以在我一开始写的代码中,当brushAll启动时,主线程会进行调用并创建bean对象,但是由于批量更新时间较长,主线程就自动销毁从而出现了以上问题。

清楚了问题后就很好解决,可以让主线程等待子线程执行完之后再进行其余操作,具体代码如下:

    @Test
    public void brushAll() throws InterruptedException {
        Thread thread = new Thread(() -> {
            List<WorkItPushLogDO> workItPushLogDOS = workItPushLogMapper.selectAll();
            for (WorkItPushLogDO item : workItPushLogDOS) {
                workItPushLogManager.encodeMobile(item);
                workItPushLogMapper.updateByPrimaryKeySelective(item);
                System.out.println("加密成功" + item.getId());
            }
        }, "brush");
        // 启动子线程
        thread.start();
        // 等待子线程执行完毕
        thread.join();
        // 主线程进入睡眠
        Thread.sleep(400000); // 400秒
        System.out.println("brush success!");
    }

新启一个子线程start,使用join()方法等待子线程结束,并让主线程进行睡眠,我这里需要刷新的数据是将近200万条,经测试在我电脑上差不多需要三百多秒,我设定了400秒就肯定足够了。

Java堆溢出

之前在测试方法中刷的数据库是测试环境的数据库,等到迭代版本正式上线后,需要将生产数据库中的数据也刷成加密。

刷生产数据库的数据就不能在@Test中调用测试方法了,就需要将刷数据的方法写成一个接口,然后配置一下网关使用postman进行调用。

接口实现我把有关线程暂停的那些方法都去掉了,因为调用接口会主动开启一个线程,不会出现@Test中的那些问题。

但是等到版本上线之后,我发现调用我写的刷数据接口后,直接报500错误:
500都是服务器内部的错误,一般就是接口写的有问题,但是我在提交之前使用测试数据库测试过了,能够成功执行,一下子没看出问题,于是我就去日志里面查看问题,发现居然是SQL报错:

原来生产数据库里面的记录数和测试数据库不一样,测试数据库那张表只有一千三百多条,而生产数据库里有七十多万条,如果一次性将其全部查出来,肯定直接内存溢出了。

于是我修改了一下方法,每次查出一千条数据进行刷取,直至将数据全部刷完,以对象类型A举例。

    public Response<Boolean> brushTel(Request<Void> request) {
        int i = 0;
        while (true) {
            List<A> aList = AMapper.selectAll(i * 1000);
            if (CollectionUtils.isEmpty(aList)) {
                break;
            }
            for (A item : aList) {
                try {
                    AManager.encodeMobile(item);
                    AMapper.updateByPrimaryKeySelective(item);
                } catch (Exception e) {
                    Loggers.BIZ.warn("刷数据出现异常:" + item.getId() , e);
                }
            }
            i++;
            if (aList.size() < 1000) {
                break;
            }
        }
        return Response.successResponse(true);
    }

来源链接:https://www.cnblogs.com/lemondu/p/18589568

© 版权声明
THE END
支持一下吧
点赞9 分享
评论 抢沙发
头像
请文明发言!
提交
头像

昵称

取消
昵称表情代码

    暂无评论内容