作者:杜红军
随着对个人信息保护意识的增强,如今,但凡在系统里出现身份证号、手机号或银行卡号等各类敏感信息,开发者均会进行加密或哈希算法处理后再储存,这已经是一个必备常识。然而,在开发实践中,我们依旧不可避免地会遇到“历史遗留问题”——系统中被忘记加密的明文敏感字段。
如果这些明文敏感字段,出现在核心服务系统里,这个系统“脱敏”改造工程将会变得十分棘手——一是因为核心系统的服务通常不能随便中断;二是由于核心系统的服务往往已应用广泛,数据量巨大,要给这样不能停的系统进行“脱敏”改造,其难度相当于要给飞行中的飞机换发动机! 结合一次“空中修引擎”的亲身经历,本篇博文带大家一起探讨在线系统 如何进行脱敏改造,让我们一起挑战、破解难题中的难题。
一、 确定脱敏数据的存储形式
敏感数据虽然在储存时要加密,但在使用时需要被转换成明文,因此,加密的过程必须是可逆的。针对在线系统中出现的明文敏感数据,我们选择了被业界广泛接受的AES256-CBC形式,增加了一个保存加密信息的字段 xxx_secure。然而,由于随机初始向量(IV)的存在,会出现同一明文敏感数据多次加密后,产生多个完全不同的密文,导致这个密文字段不可进行索引或是进行直接比对,因此,我们又额外引入了一个哈希字段 xxx_hash 用于检索。
二、 选择数据对象的映射方式
此次进行脱敏改造的在线系统部署了较好的ORM映射管理,使用了数据持久化JPA框架,基本上没有裸读写数据库的情况,因此,本次改造只需要在ORM框架层面改变实体对象的读写行为,让其变成访问相应的加密字段,即可保持对象对既有业务行为的一致,无需改动业务代码——这是本次改造实战中最值得庆幸的一件事,也从侧面再次证明数据访问层(DAO)的价值——即使是再薄的一个框架,如果自带DAO层,即使系统出现严重问题,也能带来更多切入挽救的机会。
JPA框架下通常有以下两种方式实现数据对象映射:
1. 定义converter实现AttributeConverter接口双向转化
该接口是范型实现,必须要存在2个范型类型,分别对应JAVA类型和SQL类型,并一定要求数据可以双向转化,我们在公共包里实现了一个 Encrypted AttributeConverter类型,其主要相关方法如下:
△图1:代码诠释
随后,在Entity属性上使用Convert注解,指定上述converter,即可在读写数据库时实现自动加密和解密,参考如下:
△图2:代码诠释
2. 实现自定义用户类(UserType)接口
自定义用户类接口适用于需要自动化转化哈希加密的情形,可以有效避免双向数据可逆的影响。因为JPA框架在持久化的过程中需要通过深拷贝方式克隆原始对象副本,若使用AttributeConverter接口转化哈希加密,则AttributeConverterMutabilityPlanImpl.deepCopyNotNull(T value)是深拷贝的入口,这个克隆方向最终会调用AttributeConverter.convertToEntityAttribute(String dbData)的数值来复制对象(参见“图1代码诠释”),但由于哈希加密不可逆,如果数据库数据是哈希值则无法获取明文。而当保存数据时,又会调用convertToDatabaseColumn(HashStringField attribute) 对数据做加密处理,若此时明文字段处为空字符串,则最终会导致数据丢失。
自定义用户类接口可以通过Override深拷贝方法实现JAVA type到SQL type的数据映射,该接口定义有很多方法,比如指明JAVA 类型;指明SQL 类型;SQL数据到JAVA 对象之间的互转;或者对象标识符方法,如hashCode()和equals()方法;深拷贝方法及序列化相关方法, 以上方法具体代码诠释示意如下(图3):
△图3:代码诠释
自定义用户数据接口实现之后,就可以在Entity类的属性中使用类型注解,指明属性,例如:
△图4:代码诠释
然后,就可以实现自定义用户数据类型的单向转化。但是在实践中,我们认为这个方法过于复杂,而且把明明是一个String的哈希字符串强行定义为新类型HashStringField也很怪异,经过多方考量,最终还是选择通过在getter、setter方法直接转化,而没有用自定义用户接口方案实现数据映射。
三、 开始“空中换引擎”——在线无缝脱敏
完成明文字段的自动转化,在确保业务逻辑无需大量变更之后,还要面对服务多节点多区域部署的问题,因为此时线上无可避免地会出现新旧服务并存的阶段,但由于是核心系统,不可能安排停机更新。那么,如何为运行中的系统“空中换引擎”?综合而言,为数据库表添加脱敏字段后,我们在实践中实施了三步替换方案:第一步:双写但是信任明文字段;第二步:双写但是信任密文字段;第三步:单独使用密文字段,完成后在数据库中废除明文字段,消除安全隐患。以上步骤具体实施情况如下:
第一步:双写但是信任明文字段
以Entity中的phone字段为例,首先向数据库添加相关脱敏字段,如phone_hash、phone_secure,内容可默认为空。接着,更新Entity对象并升级服务,服务获取数据时以明文字段为准,但写入时,同时更新明文和密文字段,保证升级过程中,读取的数据是准确的,再通过相关getter、setter方法转化。
△图4:代码诠释
第二步:双写但是信任密文字段
脱敏字段更新完成后,检查所有服务功能,确认升级成功。再运行脚本, set phone_secure = Encrypt(phone) ,补齐加密字段所有内容,此时 phone_secure字段中即为全量正确的加密后的数据。
然后,修改代码 ,切换信任关系,由于服务多节点、跨地域存在,此时程序需要仍然保持双写,保证新旧节点读取数据都是最新值,但是对象取值以加密字段为准,继续以phone举例,操作如下:
△图5:代码诠释
第三步:单独使用密文字段
第二步完成后,系统中所有节点已信任加密文字段,即可以进入解除对明文字段写入的步骤——单独使用密文字段,并准备从系统中废弃明文字段。此时,需要在Entity中修改映射关系的方式如下:
至此,Entity 类已恢复为干净的实体类别,并且整体看代码无其他太多变化,只是在phone字段增添了两个注解。删除明文字段之后,经一段时长的运行观察均状况正常之后,可以先重命名,然后彻底删除明文字段——“空中修引擎”般的在线系统脱敏工程宣告完成。