背景
对互联网公司来说,数据安全一直是极为重视和敏感的话题。涉及客户安全数据或者一些商业性敏感数据,如身份证号、手机号、卡号、客户号等个人信息如果被泄露出去,就会引发严重的数据安全风险。
在真实业务场景中,相关业务开发团队往往需要针对公司安全部门需求,自行实行并维护一套加解密系统,自行维护的加解密系统往往又面临着重构或修改风险。 因此希望实现一个通用的敏感数据处理框架,如何在不修改业务逻辑、业务SQL的情况下,透明化、安全低风险地实现无缝进行数据加解密改造。
加密框架实现方案比较
先通过下面列表来讨论几种加密实现方案的优缺点:
方案 | 优点 | 缺点 |
---|---|---|
开发人员自行加密 | 1. 代码侵入性多 2. 维护困难 3. 工作量大 | |
改造数据库驱动 | 1. 代码侵入性少 2. 接入成本低 | 1. 需要熟悉源代码 2. 需要和官方版本定时更新同步 |
改造ORM | 1. 代码侵入性少 2. 接入成本低 | 1. 需要熟悉源代码 2. 需要和官方版本定时更新同步 3. 多种ORM框架,需要一一支持 |
云上CASB | 1. proxy方式,对应用透明 2. 与开发语言和框架无关 3. 支持存量数据加密 | 1. proxy的性能损耗高于本地模式 2. proxy一旦宕机影响巨大 |
云数据库存储加密 | 1. 对应用透明 2. 与开发语言和框架无关 | 1. 对表空间进行加密,性能损失比较大 2. 无法支持存量数据加密 |
通过上面几种方案比较,得出实现一种加密框架至少有如下几个需求:
- 代码侵入性少
- 接入成本低
- 覆盖更多框架
- 高性能,高可用
- 支持存量数据加密
可以发现对数据库驱动层改造,相对其他几种方案缺点更少。那么是不是有一种方案,可以在不改造数据库驱动情况下,又能达到透明加解密数据的需求?
数据库访问架构
计算机领域的任何问题都可以通过增加一个间接的中间层来解决,这本身就体现了分层的重要性。比如,Unix 系统也是基于分层开发的,它可以大致上分为三层,分别是内核、系统调用、应用层。每一层都对上层封装实现细节,暴露抽象的接口来调用。
对于数据库访问也可以基于这样的软件思想来实现。由于各个厂商的数据库服务器差异比较大,因此需要通过定义一种用于执行SQL语句的API,为多种数据库提供统一访问。比如Java的JDBC,go的database,它们提供了一种基准和规范,据此可以构建更高级的工具和接口。数据库开发人员遵从这种基准和规范,编写的应用程序称之为数据库驱动。
面向切面编程
面向切面编程(AOP),是软件开发中的一个热点。通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。利用AOP可以将与业务非关联的功能剥离开来,比如权限认证,日志记录,性能监控,错误处理等。通过对指定方法执行前后进行拦截方式,实现相同功能的复用,避免对业务代码的侵入。因此AOP可以降低代码逻辑之间的耦合度,提高程序的可重用性,同时提高了开发的效率。
通过AOP对业务SQL拦截重写
假设实现一种数据库驱动XDriver,它是对XDBC的标准API具体实现,下面通过伪代码来实现通过AOP对XDriver拦截,从而对业务SQL重写。
代码语言:javascript复制public interface Database {
/**
* 执行SQL
*/
ResultSet executeQuery(String sql);
}
public class XDriver implements Database {
@Override
public ResultSet executeQuery(String sql) {
ResultSet resultSet = null;
//TODO
return resultSet;
}
}
public class XDriverIntercepter implements Database {
private Database database;
public void setDatabase(Database database) {
this.Database = database;
}
@Override
public 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:
代码语言:javascript复制SELECT address FROM account WHERE mobile=?
解析之后的为抽象语法树见下图:
将抽象语法树转换成如下图:
将抽象语法树反解析成以下SQL:
代码语言:javascript复制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去定义。具体的配置内容如下:
代码语言:javascript复制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加密,可以通过以下几个步骤来实现对加密数据平滑迁移
已上线业务改造-迁移前
对account表新增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框架的选型。
- 高性能,高可用 :通过分段锁,缓存等性能优化手段保证框架对业务性能几乎无影响。去中心化方式,将极大降低异常造成对业务线影响。
- 支持存量数据加密:通过明文列和密文列灵活的配置,实现业务系统较为安全、平滑地在明文与密文数据间的迁移。