一、写在前面

在数据安全与保密领域比较高行业场景下,某些用户隐私数据的加密存储是一个绕不开的话题。对数据进行落盘加密我们往往能轻而易举的搞定,
如果条件允许的情况下,加密字段不允许模糊查询,仅支持精准匹配,这是一件最令人愉快的事,但现实情况往往是身不由己的,我们美好的愿景总是能被产品和客户的一言以破之。
如此,数据加密后,如何对于加密数据进行查询是一个必须搬开的绊脚石。

二、我们的目标

对于加密存储的字段也能像非加密存储的字段一样进行模糊查询

三、我们的思考

加密后的字符串根本没办法直接通过SQL语句中的like关键字模糊查询。我们要模糊查询只有两种可能。
第一、用明文匹配,那必须,对加密数据进行解密,然后模糊查询;
第二、用密文直接进行匹配;
从现在的角度,除以上两条路,似乎别无它途了吧。

四、明文匹配的思考

1)、一次性加载到内存

明文匹配,我们想到的可能就是一次性把所有密文数据加载到内存中,并进行解密,然后查询的时候,直接用明文进行内存匹配。
这样做的好处是:实现起来比较简单,成本非常低。
但带来的问题是:如果个人隐私数据非常多的话,应用服务器的内存不一定够用,可能会出现OOM问题。
还有另外一个问题是:数据一致性问题。
如果用户修改了数据,数据库更新成功了,需要同步更新内存中的缓存,否则用户查询的结果可能会跟实际情况不一致。
比如:数据库更新成功了,内存中的缓存更新失败了。
或者你的应用,部署了多个服务器节点,有一部分内存缓存更新成功了,另外一部分刚好在重启,导致更新失败了。
该方案不仅可能会导致应用服务器出现OOM问题,也可能会导致系统的复杂度提升许多。
在一些局部,小范围,小数据量的场景下,可以稍微用一用,毕竟开发简单,就是保证保数据一致性即可。

2)、使用数据库函数

既然数据库中保存的是加密后的字符串,还有一种方案是使用数据库的函数解密。
我们可以使用MySQL的DES_ENCRYPT函数加密,使用DES_DECRYPT函数解密

SELECT DES_DECRYPT('密文', '秘钥');

应用系统中所有的用户隐私信息的加解密都在MySQL层实现,不存在加解密不一致的情况。
该方案中保存数据时,只对单个用户的数据进行操作,数据量比较小,性能还好。
但模糊查询数据时,每一次都需要通过DES_DECRYPT函数,把数据库中用户某个隐私信息字段的所有数据都解密了,然后再通过解密后的数据,做模糊查询。
如果该字段的数据量非常大,这样每次查询的性能会非常差。
而且,对于数据库不支持的加密算法,此方案无能为力。

五、密文匹配的思考

1)、分段保存

我们可以将一个完整的字符串,拆分成多个小的字符串。
以手机号为例:17759188517,按每3位为一组,进行拆分,拆分后的字符串为:177,775,759,591,918,188,885,851,517,这9组数据。
然后建一张表:

CREATE TABLE `encrypt_value_mapping` (
  `id` bigint NOT NULL COMMENT '系统编号',
  `ref_id` bigint NOT NULL COMMENT '关联系统编号',
  `encrypt_value` varchar(255) NOT NULL COMMENT '加密后的字符串'
) ENGINE=InnoDB  CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='分段加密映射表'

这张表有三个字段:
id:系统编号。
ref_id:主业务表的系统编号,比如用户表的系统编号。
encrypt_value:拆分后的加密字符串。
用户在写入手机号的时候,同步把拆分之后的手机号分组数据,也一起写入,可以保证在同一个事务当中,保证数据的一致性。
如果要模糊查询手机号,可以直接通过encrypt_value_mapping的encrypt_value模糊查询出用户表的ref_id,再通过ref_id查询用户信息。
具体sql如下:

select s2.id,s2.name,s2.phone
from encrypt_value_mapping s1
inner join `user` s2 on s1.ref_id=s2.id
where s1.encrypt_value = 'U2FsdGVkX19Se8cEpSLVGTkLw/yiNhcB'
limit 0,20;

这样就能轻松的通过模糊查询,搜索出我们想要的手机号了。
注意这里的encrypt_value用的等于号,由于是等值查询,效率比较高。

注意:
1、如此规则下,手机号模糊查询必须是3位数及以上。如果是3位数,则直接加密匹配,如果超过三位数,需要进行拆分成多个3位数,然后进行查询,获取每个三位数关联都存在的ref_id。
2、这里通过sql语句查询出来的手机号是加密的,在接口返回给前端之前,需要在代码中统一做解密处理。

如果除了用户手机号,还有其他的用户隐私字段需要模糊查询的场景,该怎么办?
我们可以将encrypt_value_mapping表扩展一下,增加一个type字段。
该字段表示数据的类型,比如:1.手机号 2.身份证 3.银行卡号等。
这样如果有身份证和银行卡号模块查询的业务场景,我们可以通过type字段做区分,也可以使用这套方案,将数据写入到encrypt_value_mapping表,最后根据不同的type查询出不同的分组数据。
如果业务表中的数据量少,这套方案是可以满足需求的。
但如果业务表中的数据量很大,一个手机号就需要保存9条数据,一个身份证或者银行卡号也需要保存很多条数据,这样会导致encrypt_value_mapping表的数据急剧增加,可能会导致这张表非常大。
最后的后果是非常影响查询性能。

2)、增加模糊查询字段

还是以手机模糊查询为例。
我们可以在用户表中,在手机号旁边,增加一个encrypt_phone字段。

CREATE TABLE `user` (
  `id` int NOT NULL,
  `code` varchar(20)  NOT NULL,
  `age` int NOT NULL DEFAULT '0',
  `name` varchar(30) NOT NULL,
  `height` int NOT NULL DEFAULT '0',
  `address` varchar(30)  DEFAULT NULL,
  `phone` varchar(11) DEFAULT NULL,
  `encrypt_phone` varchar(255)  DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='用户表'

然后我们在保存数据的时候,将分组之后的数据拼接起来。
还是以手机号为例:
17759188517,按每3位为一组,进行拆分,拆分后的字符串为:177,775,759,591,918,188,885,851,517,这9组数据。
分组之后,加密之后,用逗号分割之后拼接成这样的数据:
密文1,密文2,密文3,密文4,密文5,密文6,密文7,密文8,密文9
以后可以直接通过sql模糊查询字段encrypt_phone了:

select id,name,phone
from user where encrypt_phone like '%密文%'
limit 0,20;

注意这里的encrypt_value用的like。
这里为什么要用逗号分割呢?
答:是为了防止直接字符串拼接,在极端情况下,两个分组的数据,原本都不满足模糊搜索条件,但拼接在一起,却有一部分满足条件的情况发生。
当然你也可以根据实际情况,将逗号改成其他的特殊字符。
此外,其他的用户隐私字段,如果要实现模糊查询功能,也可以使用类似的方案。

六、总结

加密数据模糊查询的方案在现今加解密的技术手段没有一个新的突破的情况下,方案的设计是没有一个标准的,是多样化的。除了以上大概设计的几个方案外,还有很多可以实现的方案,无绝对优劣之分。要根据实际业务场景来选择,没有最好的方案,只有最合适的。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐