需求
今天分配到一个任务,修改数据库的敏感字段。
具体就是,以前有些数据库的敏感字段,诸如手机号、邮箱这一类的信息,在数据库中应该不存储或加密存储的,但是之前编写的时候并未加密直接存储在数据库了,现在为了安全需要这些敏感字段进行修改。
方法
修改敏感字段的方法有两种,上面也提到了。
- 一种是不存储在那张表里,关联用户Id从用户表中去查找相关的敏感信息然后填充到当前对象中;
- 另一种是对数据先进行加密,然后再存储到数据库中。
这两种办法根据不同的场景使用,如果当前表关联了用户Id就使用第一种,否则使用第二种。
问题
第一种办法在修改中没有遇到问题,问题出在第二个修改方法上。
第二种加密方法是使用的KMS
加密:
密钥管理服务(KMS)是一套密钥管理系统, 可以针对云上数据/各端上的加密需求精心设计的密码应用服务,为应用提供符合各种要求的密钥服务及极简应用加解密服务,轻松使用密钥来加密保护敏感的数据资产。
KMS密钥流程如下:
- 密钥生成 – 统一管理密钥的生成,一般要求根密钥具备较高的随机性以防止密钥被猜测、应用密钥通过安全的分散算法派生生成。
- 密钥存储 – 安全的存储密钥,如使用专用的安全存储设施或采用高强度加密保护,防止密钥的泄露和窃取。
- 密钥分发 – 确保密钥从生成、存储环境向使用环境传输的过程中不被泄露。
- 密钥注销 – 密钥生命周期完结之后,合理、安全地销毁密钥,并对销毁步骤作进行记录。
- 密钥更新 – 通过合理的密钥更替机制,降低密钥长期使用带来的暴露风险。一般要求:根密钥长期有效,具备更替能力;应用密钥定期更新,防止恶意破解;过程密钥一次一密,并通过引入时间戳、流水号等应用数据防止重放攻击。
- 密钥备份 – 保证重要密钥的备份恢复机制,在密钥丢失、灾难场景下,能够较快恢复密码服务能力,恢复时间目标(RTO)和恢复点目标(RPO)满足业务方需求。
- 密钥应用和密码运算服务 – 在具体的应用场景下,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
如有侵犯您的版权,请及时联系3500663466#qq.com(#换@),我们将第一时间删除本站数据。
暂无评论内容