前幾天,知識星球中有位小夥伴,問了我一個問題:加密的手機號如何模糊查詢?
我們都知道,在做系統設計時,考慮到系統的安全性,需要對使用者的一些個人隱私資訊,比如:登入密碼、身份證號、銀行卡號、手機號等,做加密處理,防止使用者的個人資訊被洩露。
很早之前,CSDN遭遇了SQL隱碼攻擊,導致了600多萬條明文儲存的使用者資訊被洩。
因此,我們在做系統設計的時候,要考慮要把使用者的隱私資訊加密儲存。
常見的對稱加密演演算法有 AES、SM4、ChaCha20、3DES、DES、Blowfish、IDEA、RC5、RC6、Camellia等。
目前國際主流的對稱加密演演算法是AES
,國內主推的則是SM4
。
無論是用哪種演演算法,加密前的字串,和加密後的字串,差別還是比較大的。
比如加密前的字串:蘇三說技術
,使用金鑰:123
,生成加密後的字串為:U2FsdGVkX1+q7g9npbydGL1HXzaZZ6uYYtXyug83jHA=
。
如何對加密後的字串做模糊查詢呢?
比如:假設查詢蘇三
關鍵字,加密後的字串是:U2FsdGVkX19eCv+xt2WkQb5auYo0ckyw
。
上面生成的兩個加密字串差異看起來比較大,根本沒辦法直接通過SQL語句中的like關鍵字模糊查詢。
那我們該怎麼實現加密的手機號的模糊查詢功能呢?
實現這個功能,我們第一個想到的辦法可能是:把個人隱私資料一次性載入到記憶體中快取起來,然後在記憶體中先解密,然後在程式碼中實現模糊搜尋的功能。
這樣做的好處是:實現起來比較簡單,成本非常低。
但帶來的問題是:如果個人隱私資料非常多的話,應用伺服器的記憶體不一定夠用,可能會出現OOM問題。
還有另外一個問題是:資料一致性問題。
如果使用者修改了手機號,資料庫更新成功了,需要同步更新記憶體中的快取,否則使用者查詢的結果可能會跟實際情況不一致。
比如:資料庫更新成功了,記憶體中的快取更新失敗了。
或者你的應用,部署了多個伺服器節點,有一部分記憶體快取更新成功了,另外一部分剛好在重啟,導致更新失敗了。
該方案不僅可能會導致應用伺服器出現OOM問題,也可能會導致系統的複雜度提升許多,總體來說,有點得不償失。
既然資料庫中儲存的是加密後的字串,還有一種方案是使用資料庫的函數解密。
我們可以使用MySQL的DES_ENCRYPT函數加密,使用DES_DECRYPT函數解密:
SELECT
DES_DECRYPT('U2FsdGVkX1+q7g9npbydGL1HXzaZZ6uYYtXyug83jHA=', '123');
應用系統重所有的使用者隱私資訊的加解密都在MySQL層實現,不存在加解密不一致的情況。
該方案中儲存資料時,只對單個使用者的資料進行操作,資料量比較小,效能還好。
但模糊查詢資料時,每一次都需要通過DES_DECRYPT函數,把資料庫中使用者某個隱私資訊欄位的所有資料都解密了,然後再通過解密後的資料,做模糊查詢。
如果該欄位的資料量非常大,這樣每次查詢的效能會非常差。
我們可以將一個完整的字串,拆分成多個小的字串。
以手機號為例:18200256007,按每3位為一組,進行拆分,拆分後的字串為:182,820,200,002,025,256,560,600,007,這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='分段加密對映表'
這張表有三個欄位:
使用者在寫入手機號的時候,同步把拆分之後的手機號分組資料,也一起寫入,可以保證在同一個事務當中,保證資料的一致性。
如果要模糊查詢手機號,可以直接通過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用的等於號,由於是等值查詢,效率比較高。
注意:這裡通過sql語句查詢出來的手機號是加密的,在介面返回給前端之前,需要在程式碼中統一做解密處理。
為了安全性,還可以將加密後的明文密碼,用*
號增加一些干擾項,防止手機號被洩露,最後展示給使用者的內容,可以顯示成這樣的:182***07
。
如果除了使用者手機號,還有其他的使用者隱私欄位需要模糊查詢的場景,該怎麼辦?
我們可以將encrypt_value_mapping表擴充套件一下,增加一個type
欄位。
該欄位表示資料的型別,比如:1.手機號 2.身份證 3.銀行卡號等。
這樣如果有身份證和銀行卡號模組查詢的業務場景,我們可以通過type欄位做區分,也可以使用這套方案,將資料寫入到encrypt_value_mapping表,最後根據不同的type查詢出不同的分組資料。
如果業務表中的資料量少,這套方案是可以滿足需求的。
但如果業務表中的資料量很大,一個手機號就需要儲存9條資料,一個身份證或者銀行卡號也需要儲存很多條資料,這樣會導致encrypt_value_mapping表的資料急劇增加,可能會導致這張表非常大。
最後的後果是非常影響查詢效能。
那麼,這種情況該怎麼辦呢?
如果資料量多的情況下,將所有使用者隱私資訊欄位,分組之後,都集中到一張表中,確實非常影響查詢的效能。
那麼,該如何優化呢?
答:我們可以增加模糊查詢欄位。
還是以手機模糊查詢為例。
我們可以在使用者表中,在手機號旁邊,增加一個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='使用者表'
然後我們在儲存資料的時候,將分組之後的資料拼接起來。
還是以手機號為例:
18200256007,按每3位為一組,進行拆分,拆分後的字串為:182,820,200,002,025,256,560,600,007,這9組資料。
分組之後,加密之後,用逗號分割之後拼接成這樣的資料:,U2FsdGVkX19Se8cEpSLVGTkLw/yiNhcB,U2FsdGVkX1+qysCDyVMm/aYXMRpCEmBD,U2FsdGVkX19oXuv8m4ZAjz+AGhfXlsQk,U2FsdGVkX19VFs60R26BLFzv5nDZX40U,U2FsdGVkX19XPO0by9pVw4GKnGI3Z5Zs,U2FsdGVkX1/FIIaYpHlIlrngIYEnuwlM,U2FsdGVkX19s6WTtqngdAM9sgo5xKvld,U2FsdGVkX19PmLyjtuOpsMYKe2pmf+XW,U2FsdGVkX1+cJ/qussMgdPQq3WGdp16Q。
以後可以直接通過sql模糊查詢欄位encrypt_phone了:
select id,name,phone
from user where encrypt_phone like '%U2FsdGVkX19Se8cEpSLVGTkLw/yiNhcB%'
limit 0,20;
注意這裡的encrypt_value用的like
。
這裡為什麼要用逗號分割呢?
答:是為了防止直接字串拼接,在極端情況下,兩個分組的資料,原本都不滿足模糊搜尋條件,但拼接在一起,卻有一部分滿足條件的情況發生。
當然你也可以根據實際情況,將逗號改成其他的特殊字元。
此外,其他的使用者隱私欄位,如果要實現模糊查詢功能,也可以使用類似的方案。
最後說一句,雖說本文介紹了多種加密手機號實現模糊查詢功能的方案,但我們要根據實際業務場景來選擇,沒有最好的方案,只有最合適的。
最後歡迎大家加入蘇三的知識星球【Java突擊隊】,一起學習。
星球中有很多獨家的乾貨內容,比如:Java後端學習路線,分享實戰專案,原始碼分析,百萬級系統設計,系統上線的一些坑,MQ專題,真實面試題,每天都會回答大家提出的問題,免費修改簡歷,免費回答工作中的問題。
星球目前開通了9個優質專欄:技術選型、系統設計、踩坑分享、工作實戰、底層原理、Spring原始碼解讀、痛點問題、高頻面試題 和 效能優化。
加入星球如果不滿意,3天內包退。