前言
開始的時(shí)候遇到了一個(gè)問題?敏感字段數(shù)據(jù)是加密存儲(chǔ)在數(shù)據(jù)庫的表中,如果需要對(duì)這些敏感字段進(jìn)行模模糊查詢,還用原來的通過sql的where從句的like來模糊查詢的方式肯定是不行的,那么應(yīng)該怎么實(shí)現(xiàn)呢?
場(chǎng)景分析
假如有類似這樣的一個(gè)場(chǎng)景:有一個(gè)人員管理的功能,人員信息列表的主要字段有名稱、性別、手機(jī)號(hào)碼、等,可以對(duì)任意一條數(shù)據(jù)進(jìn)行增、刪、改、查,其中姓名、手機(jī)號(hào)碼字段要支持模糊查詢。
簡單分析一個(gè)場(chǎng)景,可以知道:手機(jī)號(hào)碼是敏感數(shù)據(jù),這些字段的數(shù)據(jù)是要加密存儲(chǔ)在數(shù)據(jù)庫里,在頁面上展示的時(shí)候需要進(jìn)行脫敏處理的。
如果用戶想要查詢真實(shí)姓名是包含有“李四”的所有人員信息,可以在頁面上輸入一個(gè)關(guān)鍵字,如“李四”,點(diǎn)擊開始查詢后,這個(gè)參數(shù)會(huì)傳遞到后臺(tái),后臺(tái)會(huì)執(zhí)行一條sql,如“select * from sys_person where real_name like ‘%李四%’”,執(zhí)行結(jié)果中包含了所有用戶真實(shí)姓名包含有“李四”的所有數(shù)據(jù)記錄,如“李四”,“李四娘”等。
如果用戶要查詢手機(jī)號(hào)碼尾號(hào)是“7507”的用戶,后臺(tái)執(zhí)行類似與姓名模糊查詢的sql,"select * from sys_person where phone like '%7507'",肯定是得不到正確的結(jié)果的,因?yàn)槭謾C(jī)號(hào)碼字段在數(shù)據(jù)庫中的數(shù)據(jù)是加密后的結(jié)果,而‘7507’是明文,即模糊查詢關(guān)鍵字與實(shí)際存儲(chǔ)的數(shù)據(jù)不一致。
實(shí)現(xiàn)方案
分詞密文映射表
這種方法是主流的方法。新建一張分詞密文映射表,在敏感字段數(shù)據(jù)新增、修改的后,對(duì)敏感字段進(jìn)行分詞組合,如“15503777507”的分詞組合有“155”、“0377”、“7507”等,再對(duì)每個(gè)分詞進(jìn)行加密,建立起敏感字段的分詞密文與目標(biāo)數(shù)據(jù)行主鍵的關(guān)聯(lián)關(guān)系;在處理模糊查詢的時(shí)候,對(duì)模糊查詢關(guān)鍵字進(jìn)行加密,用加密后的模糊查詢關(guān)鍵字,對(duì)分詞密文映射表進(jìn)行l(wèi)ike查詢,得到目標(biāo)數(shù)據(jù)行的主鍵,再以目標(biāo)數(shù)據(jù)行的主鍵為條件返回目標(biāo)表進(jìn)行精確查詢。
淘寶、阿里、拼多、京東等大廠對(duì)用戶敏感數(shù)據(jù)加密后支持模糊查詢都是這樣的原理,下面是幾個(gè)大廠的敏感字段模糊查詢方案說明,有興趣可以了解一下:
淘寶密文字段檢索方案
阿里巴巴文字段檢索方案
拼多多密文字段檢索方案
京東密文字段檢索方案
這種方法的優(yōu)點(diǎn)就是原理簡單,實(shí)現(xiàn)起來也不復(fù)雜,但是有一定的局限性,算是一個(gè)對(duì)性能、業(yè)務(wù)相折中的一個(gè)方案,相比較之下,在能想的方法中,比較推薦這種方法,但是要特別注意的是,對(duì)模糊查詢的關(guān)鍵字的長度,要在業(yè)務(wù)層面進(jìn)行限制;以手機(jī)號(hào)為例,可以要求對(duì)模糊查詢的關(guān)鍵字是四位或者是五位,具體可以再根據(jù)具體的場(chǎng)景進(jìn)行詳細(xì)劃分。
為什么要增加這樣的限制呢?因?yàn)槊魑募用芎箝L度為變長,有額外的存儲(chǔ)成本和查詢性能成本,分詞組合越多,需要的存儲(chǔ)空間以及所消耗的查詢性能成本也就更大,并且分詞越短,被硬破解的可能性也就越大,也會(huì)在一定程度上導(dǎo)致安全性降低;
環(huán)境配置
jdk版本:1.8開發(fā)工具:Intellij iDEA 2020.1
springboot:2.3.9.RELEASE
mybatis-spring-boot-starter:2.1.4
依賴配置
示例主要用到了SpringAop,加密是對(duì)稱加密,用到了hutool工具包里的加密解密工具類,也可以使用自己封裝的加密解密工具類。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.3</version>
</dependency>
代碼實(shí)現(xiàn)
1、新建分詞密文映射表;
如果是多個(gè)模糊查詢的字段,可以共用在一張分詞密文映射表中擴(kuò)展多個(gè)字段,以示例中的人員管理功能為例,新建sys_person_phone_encrypt表(人員的手機(jī)號(hào)碼分詞密文映射表),用于存儲(chǔ)人員id與分詞組合密文的映射關(guān)系
(
id bigint auto_increment comment '主鍵' primary key,
person_id int not null comment '關(guān)聯(lián)人員信息表主鍵',
phone_key varchar(500) not null comment '手機(jī)號(hào)碼分詞密文'
)
comment '人員的手機(jī)號(hào)碼分詞密文映射表';
2、敏感字段數(shù)據(jù)在保存入庫的時(shí)候,對(duì)敏感字段進(jìn)行分詞組合并加密碼,存儲(chǔ)在分詞密文映射表;
在注冊(cè)人員信息的時(shí)候,先取出通過AOP進(jìn)行加密過的手機(jī)號(hào)碼進(jìn)行解密;手機(jī)號(hào)碼解密之后,對(duì)手機(jī)號(hào)碼按照連續(xù)四位進(jìn)行分詞組合,并對(duì)每一個(gè)手機(jī)號(hào)碼的分詞進(jìn)行加密,最后把所有的加密后手機(jī)號(hào)碼分詞拼接成一個(gè)字符串,與人員id一起保存到人員的手機(jī)號(hào)碼分詞密文映射表;
this.personDao.insert(person);
String phone = this.decrypt(person.getPhoneNumber());
String phoneKeywords = this.phoneKeywords(phone);
this.personDao.insertPhoneKeyworkds(person.getId(),phoneKeywords);
return person;
}
private String phoneKeywords(String phone) {
String keywords = this.keywords(phone, 4);
System.out.println(keywords.length());
return keywords;
}
//分詞組合加密
private 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(this.encrypt(sub1));
if (end == word.length()) {
break;
}
}
return sb.toString();
}
public String encrypt(String val) {
//這里特別注意一下,對(duì)稱加密是根據(jù)密鑰進(jìn)行加密和解密的,加密和解密的密鑰是相同的,一旦泄漏,就無秘密可言,
//“fanfu-csdn”就是我自定義的密鑰,這里僅作演示使用,實(shí)際業(yè)務(wù)中,這個(gè)密鑰要以安全的方式存儲(chǔ);
byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DES.getValue(), "fanfu-csdn".getBytes()).getEncoded();
SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.DES, key);
String encryptValue = aes.encryptBase64(val);
return encryptValue;
}
public String decrypt(String val) {
//這里特別注意一下,對(duì)稱加密是根據(jù)密鑰進(jìn)行加密和解密的,加密和解密的密鑰是相同的,一旦泄漏,就無秘密可言,
//“fanfu-csdn”就是我自定義的密鑰,這里僅作演示使用,實(shí)際業(yè)務(wù)中,這個(gè)密鑰要以安全的方式存儲(chǔ);
byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DES.getValue(), "fanfu-csdn".getBytes()).getEncoded();
SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.DES, key);
String encryptValue = aes.decryptStr(val);
return encryptValue;
}
3、模糊查詢的時(shí)候,對(duì)模糊查詢關(guān)鍵字進(jìn)行加密,以加密后的關(guān)鍵字密文為查詢條件,查詢密文映射表,得到目標(biāo)數(shù)據(jù)行的id,再以目標(biāo)數(shù)據(jù)行id為查詢條件,查詢目標(biāo)數(shù)據(jù)表;
根據(jù)手機(jī)號(hào)碼的四位進(jìn)行模糊查詢的時(shí)候,以加密后模糊查詢的關(guān)鍵字為條件,查詢sys_person_phone_encrypt表(人員的手機(jī)號(hào)碼分詞密文映射表),得到人員信息id;再以人員信息id,查詢?nèi)藛T信息表;
if (phoneVal != null) {
return this.personDao.queryByPhoneEncrypt(this.encrypt(phoneVal));
}
return this.personDao.queryList(phoneVal);
}
select * from sys_person where id in
(select person_id from sys_person_phone_encrypt
where phone_key like concat('%',#{phoneVal},'%'))
</select>