敏感加密信息模糊查询解决方案
敏感加密信息模糊查询解决方案
在数据库中存储加密敏感信息时,如何实现模糊查询是一个常见的技术问题。本文将介绍几种解决方案,并重点推荐"分词密文映射表"方法,该方法通过建立分词密文映射表来实现模糊查询,同时提供了具体的代码实现和环境配置说明。
前言
在实际应用中,敏感字段数据(如手机号码、身份证号码等)通常需要加密存储在数据库中。然而,这种加密存储方式给模糊查询带来了挑战。传统的SQL LIKE查询方式在这种场景下不再适用,因为查询关键字是明文,而数据库中存储的是加密后的数据。本文将探讨几种解决方案,并重点推荐一种在实际生产环境中较为实用的方法。
场景分析
假设有一个人员管理功能,主要字段包括姓名、性别、用户账号、手机号码、身份证号码、家庭住址和注册日期等。其中,姓名、身份证号码和手机号码需要支持模糊查询。由于手机号码、身份证号码和家庭住址等字段包含敏感信息,这些字段的数据需要加密存储在数据库中,并在页面展示时进行脱敏处理。
例如,用户想要查询所有真实姓名包含"张三"的人员信息,可以在页面上输入关键字"张三"。后台会执行类似以下的SQL查询:
SELECT * FROM sys_person WHERE real_name LIKE '%张三%'
但是,如果用户要查询手机号码尾号为"0537"的用户,直接使用类似SQL查询:
SELECT * FROM sys_person WHERE phone LIKE '%0537'
将无法得到正确结果,因为手机号码字段在数据库中存储的是加密后的数据,而查询关键字是明文。
实现方案
下面介绍几种可能的解决方案:
第一种:先解密再查询
这种方法是在内存中解密所有目标表的数据,然后遍历解密后的数据,与模糊查询关键字进行比较,筛选出包含关键字的数据行。虽然这种方法容易想到,但存在明显的缺点:如果数据量很大,很容易导致内存溢出,因此不推荐在生产环境中使用。
第二种:明文映射表
创建一张映射表,存储敏感字段解密后的数据与目标表主键的映射关系。查询时先对明文映射表进行模糊查询,获取符合条件的目标数据主键,再根据主键查询目标表。这种方法实际上相当于没有对敏感字段进行加密存储,违背了加密存储的初衷,因此也不推荐在生产环境中使用。
第三种:数据库层面解密查询
在执行查询SQL时对敏感字段先解密,然后再执行LIKE查询。例如:
SELECT * FROM sys_person WHERE AES_DECRYPT(phone, 'key') LIKE '%0537'
这种方法的优点是实现成本低,但存在明显缺点:无法利用数据库索引优化查询,且某些数据库可能无法保证加解密算法的一致性,导致加密和解密出现不匹配的问题。因此,这种方法也不推荐在生产环境中使用。
第四种:分词密文映射表
这种方法是对第二种方法的优化。在敏感字段数据新增或修改后,对敏感字段进行分词组合,例如"15503770537"可以分为"155"、"0377"、"0537"等,然后对每个分词进行加密,建立敏感字段分词密文与目标数据行主键的关联关系。查询时,对模糊查询关键字进行加密,用加密后的关键字对分词密文映射表进行LIKE查询,获取目标数据行的主键,再根据主键精确查询目标表。
例如,手机号"13518543037"按4个字符一组分词后会得到以下8组分词:
- 1351
- 3518
- 5185
- 1854
- 8543
- 5430
- 4303
- 3037
将这8组分词分别加密后,组合起来作为敏感字段的检索关键字。
这种方法的优点是原理简单,实现起来也不复杂,但有一定的局限性。建议在业务层面限制模糊查询关键字的长度,例如手机号可以要求4位或5位,以平衡存储成本、查询性能和安全性。
环境配置
- JDK版本:1.8
- 开发工具:IntelliJ IDEA 2020.1
- Spring Boot版本:2.3.9.RELEASE
- MyBatis版本:2.1.4
依赖配置
示例主要用到了Spring AOP,加密是对称加密,用到了Hutool工具包里的加密解密工具类。也可以使用自己封装的加密解密工具类。
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.21</version>
</dependency>
代码实现
- 新建分词密文映射表
如果是多个模糊查询的字段,可以共用一张分词密文映射表。以人员管理功能为例,新建sys_person_phone_encrypt
表,用于存储人员ID与分词组合密文的映射关系。
-- 信息基础表
CREATE TABLE `sys_person` (
`id` bigint NOT NULL COMMENT '编号',
`name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '姓名',
`age` int DEFAULT NULL COMMENT '年龄',
`phone` varchar(125) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '手机号',
`phone_cipher` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '手机号密文',
`id_card` varchar(225) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '身份证号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户基本信息';
-- 敏感字段映射表
CREATE TABLE `sys_person_phone_encrypt` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`person_id` bigint NOT NULL COMMENT '关联人员信息表主键',
`phone_key` varchar(500) COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号码分词密文',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1704319384342601730 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='人员的手机号码分词密文映射表';
- 工具类
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import cn.hutool.crypto.symmetric.SymmetricCrypto;
public class SensitiveWordUtil {
private final static String privateKey = "RiceColor";
/**
* 加密后的组合
*/
public static String phoneKeywords(String phone) {
String keywords = keywords(phone, 4);
System.out.println(keywords.length());
return keywords;
}
/**
* 分词组合加密
*/
public static String keywords(String word, int len) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < word.length(); i++) {
int start = i;
int end = i + len;
String sub1 = word.substring(start, end);
sb.append(encrypt(sub1));
if (end == word.length()) {
break;
}
}
return sb.toString();
}
/**
* 加密
*/
public static String encrypt(String val) {
byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DES.getValue(), privateKey.getBytes()).getEncoded();
SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.DES, key);
String encryptValue = aes.encryptBase64(val);
return encryptValue;
}
/**
* 解密
*/
public static String decrypt(String val) {
byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DES.getValue(), privateKey.getBytes()).getEncoded();
SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.DES, key);
String encryptValue = aes.decryptStr(val);
return encryptValue;
}
}
- 模糊查询实现
模糊查询时,对模糊查询关键字进行加密,以加密后的关键字密文为查询条件,查询密文映射表,得到目标数据行的ID,再以目标数据行ID为查询条件,查询目标数据表。
例如,根据手机号码的四位进行模糊查询时,以加密后的模糊查询关键字为条件,查询sys_person_phone_encrypt
表,得到人员信息ID;再以人员信息ID,查询人员信息表。