問題
問題來自V2EX:如何生成固定長度唯一隨機字串?
不過需求後來有修改過,原始的需求略有不同,所以我的回答與現在的問題不太對得上,這裡以原需求為基礎重新提出這個問題,並且加上一些更有代表性的條件。
條件有以下幾個:
- 串長固定為8位,大小寫字母加數字組合
- 串內容隨機,不可被猜測,不可重複
- 目標資料量不超過一千萬
- 不需要資料庫
分析
其實重點在不需要資料庫,否則直接用一個隨機數生成器生成一千萬個隨機串放到資料庫裡直接用就好了。
如果不依賴資料庫的話,用任何隨機數生成器,都不能保證絕對不重複——雖然概率非常低,比如8位大小寫字母數字組合的空間超過14位十進位制數,一千萬不超過7位十進位制數,即低於千萬分之一——但理論上仍然可能發生。
要保持唯一就需要自增ID,但是如果只是簡單地用自增ID又會導致可能被猜測,雖然可以把組合邏輯搞得複雜一點,但資料量大了還是有一定的可能性被猜出——不要對自己想的簡單邏輯有太高的自信是一個很重要的安全原則。
實現
基本如我在評論裡說的那方法:
- 8位大小寫字母加數字轉成二進位制大概是47位多,一千萬轉成二進位制不到27位
- 所以可以用一千萬以內的自增ID,加上20位二進位制的隨機數,組合成一個47位的二進位制數
- 因為上面的組合是可猜測的,所以需要進行一次加密,用一個自定義密碼對這個數進行RC4加密
- 最後用BASE62轉換為大小寫字母加數字的字串
恢復ID的方法:
- 把字串轉成47位二進位制數
- 用RC4解密
- 去掉隨機數部分,轉為整數即為原始ID
關鍵點:
- 隨機數與ID的組合方式,這個與加密演算法是相關的
- 加密演算法的選擇,必須適合這樣的需求,即消除可猜測性
綜合這兩點,加密演算法的選擇範圍就不大了,大部分常用的分組加密演算法(一般都是128位分組,老一些的TEA也是64位,即使DES也需要56位)都肯定是沒辦法用的,只能用弱一些的短分組加密演算法,比如以位元組為單位進行加密的RC4。
但是因為47位不是剛好整數字節,所以要分成40位和7位兩組,其中40位由27位ID與13位隨機數分散組合後進行RC4加密,再加7位隨機陣列成47位二進位制數。
程式碼
初始化引數包括:
- key: RC4加密金鑰
- chars: 目標字串可用字符集合,不可重複
- length: 目標字串長度,目標字串轉為二進位制不超過64位無符號整數,即:
log(pow(len(chars), length)) / log(2) <= 64
- bits_id: ID的二進位制位數,不能超過目標串轉為二進位制位數除以8取整後乘以7的值,即:
bits_id < int(log(pow(len(chars), length)) / log(2) / 8) * 7
,之所以要如此,是為了保證加密的每個位元組至少有一位隨機數,確保RC4加密後仍然不可猜測 - secure_level: 取值0-4,為每個位元組中隨機數的位數,越多越安全,0則不含隨機數,超過4位則意義不大,僅作初始化時檢查之用,如果目標串長度設定不符合這個要求會報異常
主要功能函式是兩個:
- encrypt: ID轉目標串
- decrypt: 字串轉ID
完整程式碼在GayHub。
之所以有興趣研究這個東西,是因為這種需求其實還挺有用的。比如與微信或支付寶之類的支付平臺對接時都需要一個唯一的訂單號,但是通常客戶又不希望把自己的內部訂單ID曝露在外,所以都需要轉換一下,當然這類應用都有資料庫,只要生成一個隨機串再查一下資料庫有沒有重複即可。還有像短連結應用也是可以用的(用七位字元就可以支援到41位二進位制ID,secure_level=0)。
然而畢竟還是要多查一次資料庫,這種事情讓CPU來幹可能會更省資源一些。