写点什么

5 分钟 get 一个技术点!揭秘一种加密框架的技术实现

作者:Java-fenn
  • 2022 年 9 月 14 日
    湖南
  • 本文字数:4032 字

    阅读完需:约 13 分钟

导语


本文推选自腾讯云开发者社区-【技思广益 · 腾讯技术人原创集】专栏。该专栏是腾讯云开发者社区为腾讯技术人与广泛开发者打造的分享交流窗口。栏目邀约腾讯技术人分享原创的技术积淀,与广泛开发者互启迪共成长。本文作者是腾讯高级开发工程师杨波。


背景


对互联网公司来说,数据安全一直是极为重视和敏感的话题。涉及客户安全数据或者一些商业性敏感数据,如身份证号、手机号、卡号、客户号等个人信息如果被泄露出去,就会引发严重的数据安全风险。


在真实业务场景中,相关业务开发团队往往需要针对公司安全部门需求,自行实行并维护一套加解密系统,自行维护的加解密系统往往又面临着重构或修改风险。因此希望实现一个通用的敏感数据处理框架,如何在不修改业务逻辑、业务 SQL 的情况下,透明化、安全低风险地实现无缝进行数据加解密改造。


加密框架实现方案比较先通过下面列表来讨论几种加密实现方案的优缺点:


通过上面几种方案比较,得出实现一种加密框架至少有如下几个需求:


代码侵入性少。


接入成本低。


覆盖更多框架。


高性能,高可用。


支持存量数据加密。


可以发现对数据库驱动层改造,相对其他几种方案缺点更少。那么是不是有一种方案,可以在不改造数据库驱动情况下,又能达到透明加解密数据的需求?


数据库访问架构计算机领域的任何问题都可以通过增加一个间接的中间层来解决,这本身就体现了分层的重要性。比如,Unix 系统也是基于分层开发的,它可以大致上分为三层,分别是内核、系统调用、应用层。每一层都对上层封装实现细节,暴露抽象的接口来调用。


对于数据库访问也可以基于这样的软件思想来实现。由于各个厂商的数据库服务器差异比较大,因此需要通过定义一种用于执行 SQL 语句的 API,为多种数据库提供统一访问。比如 Java 的 JDBC,go 的 database,它们提供了一种基准和规范,据此可以构建更高级的工具和接口。数据库开发人员遵从这种基准和规范,编写的应用程序称之为数据库驱动。


(一)面向切面编程面向切面编程(AOP),是软件开发中的一个热点。通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。利用 AOP 可以将与业务非关联的功能剥离开来,比如权限认证,日志记录,性能监控,错误处理等。通过对指定方法执行前后进行拦截方式,实现相同功能的复用,避免对业务代码的侵入。因此 AOP 可以降低代码逻辑之间的耦合度,提高程序的可重用性,同时提高了开发的效率。


(二)通过 AOP 对业务 SQL 拦截重写假设实现一种数据库驱动 XDriver,它是对 XDBC 的标准 API 具体实现,下面通过伪代码来实现通过 AOP 对 XDriver 拦截,从而对业务 SQL 重写。


public interface Database {/*** 执行 SQL*/ResultSet executeQuery(String sql);}


public class XDriver implements Database {@Overridepublic ResultSet executeQuery(String sql) {ResultSet resultSet = null;//TODOreturn resultSet;}}


public class XDriverIntercepter implements Database {private Database database;


public void setDatabase(Database database) {    this.Database = database;}

@Overridepublic ResultSet executeQuery(String sql) { String newSql = rewriteSql(sql); return database.executeQuery(newSql);}
/** * 重写SQL */private String rewriteSql(String sql) { String newSql = ""; //TODO return newSql;}
复制代码


}如上代码所示假设有一张表叫 account,需要对表字段 mobile,address 加密,可以做如下处理:


在 account 表中新增 mobile_encryted,address_encrypted 字段。


通过 XDriverIntercepter 对 XDriver 的 executeQuery 方法拦截进行重写 SQL。


重写 SQL 由于 SQL 是一门完善的编程语言,因此对 SQL 的语法进行解析,与解析其他编程语言(如:Java 语言、C 语言、Go 语言等)并无本质区别。


(一)抽象语法树 SQL 解析过程分为词法解析和语法解析 。


词法分析将 SQL 拆解为不可再分的原子符号,称为 Token。其中 Token 中包含关键字(也称符号)和非关键字。


语法分析就是生成抽象语法树的过程。


例如,以下 SQL:


SELECT address FROM account WHERE mobile=?解析之后的为抽象语法树见下图:


将抽象语法树转换成如下图:


将抽象语法树反解析成以下 SQL:


SELECT address_encrypted AS address FROM account WHERE mobile_encrypted=?并发控制


在高并发场景下,不希望对 SQL 重复解析,这样会影响性能。但如果只是简单对 SQL 解析进行互斥,那么在高并发场景下,会造成大量请求处于阻塞等待状态。因此需要对并发控制做优化:


通过分段锁的方式,减少锁的粒度,提高效率。


SQL 解析结果放入缓存,避免重复解析。


(二)缓存淘汰策略业务 SQL 复杂多变,如果对每种 SQL 解析结果都缓存,会影响到内存占用。因此需要对不同的 SQL 有相应的缓存淘汰策略:


参数化查询 SQL 解析结果永久缓存。


字符串拼接 SQL 解析结果默认缓存 1 秒,如果 1 秒内再次被访问,将会刷新淘汰时间。缓存 1 秒避免高并发场景下大量重复解析 SQL 造成的内存压力。


因此建议使用参数化查询 SQL 提高性能。


配置管理通过 AOP 拦截,解析,重写业务 SQL,实现透明化对数据加密。通过并发和缓存控制保证框架的高性能。这样基本实现一个加密框架基本功能。然而对于业务使用,还有很多个性化需求。其中比较重要的有如下几点:


加密算法


密钥获取


定义需要加密的表和字段


以上 1 和 2 除了默认实现方式,还需要支持自定义算法扩展功能。


因此需要定义一种方式,将上述配置集中于一起,可以更加有效进行管理。


(一)SPI 机制 Service Provider Interface (SPI) 是一种为了被第三方实现或扩展的 API。它可以用于实现框架扩展或组件替换。用户通过实现框架提供的相应接口,动态将用户自定义的实现类加载其中,从而在保持框架架构完整性与功能稳定性的情况下,满足用户不同场景的实际需求。


框架提供了内置的加密和密钥获取实现类,用户只需进行配置即可使用;另一方面,为了满足用户不同场景的需求,还开放了相关加密和密钥获取接口,用户可依据接口提供具体实现类。再进行简单配置,即可让框架调用用户自定义的加解密方案:


EncryptAlgorithm 用于实现自定义加密算法:该接口提供 encrypt(),decrypt()两种方法。在用户进行 INSERT, DELETE, UPDATE 时,框架根据配置规则,调用 encrypt()将数据加密后存储到数据库, 而在 SELECT 时,则调用 decrypt()方法将从数据库中取出的加密数据进行逆向解密,最终将原始数据返回给用户。


KeyGenerate 用于实现自定义密钥获取:该接口提供 generate()方法。由于安全考虑,并不推荐加密密钥简单放在本地,一旦密钥泄漏将有可能造成数据泄漏的风险。因此建议将密钥中心化托管处理,然后在具体实现类通过 generate()远程获取密钥。


除了以上接口,后续也可以加入数据脱敏等接口。


(二)配置方式定义尽管通过 SPI 机制可以满足用户个性化需求,然而用户对于如何将自己的实现类以及其它规则通过编码方式配置到框架中,依然需要学习的成本。因此需要定义一种配置方式,使用户只需要参考使用文档,简单配置就可以使用框架。由于 yaml 是目前比较通用的配置格式,框架的配置也是基于 yaml 去定义。具体的配置内容如下:


encrypt_rule:


query_with_cipher_column: true # 是否使用密文列查询 encrypt_algorithms: # 定义加密算法


- name: AES-CBC # 加密算法名称 props: # 自定义参数,


key1: xx #


encrypt_key_generates: # 定义密钥生成


- name: LOCAL # 密钥生成名


props: # 自定义参数,


key1: xx #


encryptors: # 加密模块设置


encryptor-a:


algorithm: # 加密算法


name: AES-CBC # 指定使用的加密算法


symmetric_key: # 密钥生成


name: LOCAL # 指定使用的密钥生成


tables: # 加密表设置


account: # 表名


columns:mobile: #逻辑列


plain_column: mobile # 明文列


cipher_column: mobile_encrypted # 密文列


encryptor: encryptor-a # 指定使用的加密模块


key_type: mobile # 列类型,每个类型对应一个密钥(三)SQL 处理流程因此在不考虑并发场景,在增加配置管理情况下,框架对一条 SQL 处理流程将转换成如下图:


存量数据加密对于已上线且存在历史明文数据的业务,需要实现业务系统较为安全、平滑地在明文与密文数据间的迁移。


在配置中有定义以下三个参数:


query_with_cipher_column 是否使用密文列查询。


plain_column 明文列。


cipher_column 密文列。


假设有一张历史旧表叫 account,需要对字段 mobile,address 加密,可以通过以下几个步骤来实现对加密数据平滑迁移


(一)已上线业务改造-迁移前对 acco unt 表新增 mobile_encrypted,address_encrypted 用于存放密文数据,并做如下配置:


plain_column 设为 mobile,address。


cipher_column 设为 mobile_encrypted,address_encrypted。


query_with_cipher_column 设为 false。


此时数据处理流程将如下图:


(二)已上线业务改造-迁移中通过上图可以看到,当 query_with_cipher_column 设为 false 时,明文列和密文列双写,通过明文列查询。用户此时可以通过脚本,将存量数据清洗加密。然后将 query_with_cipher_column 设为 true。


此时数据处理流程将如下图:


(三)已上线业务改造-迁移后通过上图可以看到,当 query_with_cipher_column 设为 true 时,明文列和密文列双写,通过密文列查询。当系统运行一段时间稳定以后,此时可以将 account 表中明文列删除,并将配置中的 plain_column 删除。最终数据只会被加密存储在密文列。


此时数据处理流程将如下图:


(四)已上线业务改造流程因此对已上线业务数据加密改造流程如下图:


总结现在再总结文章开头提到几点需求,看看是如何解决的:


代码侵入性少:通过 AOP 方式将 SQL 重写,加解密逻辑与业务代码和数据库框架独立解耦。


接入成本低:用户无需修改原有业务逻辑,只需要进行少量修改和配置,就可以将框架集成进来。


覆盖更多框架 :基于数据库驱动层的拦截,因此不影响上层 ORM 框架的选型。


高性能,高可用 :通过分段锁,缓存等性能优化手段保证框架对业务性能几乎无影响。去中心化方式,将极大降低异常造成对业务线影响。


支持存量数据加密 : 通过明文列和密文列灵活的配置,实现业务系统较为安全、平滑地在明文与密文数据间的迁移。

用户头像

Java-fenn

关注

需要Java资料或者咨询可加我v : Jimbye 2022.08.16 加入

还未添加个人简介

评论

发布
暂无评论
5分钟get一个技术点!揭秘一种加密框架的技术实现_Java_Java-fenn_InfoQ写作社区