Python 根據id生成唯一碼
最近業務中遇到需要分享某個文案,複製文案開啟APP需要提取文案中包含的id,但又不想明文暴露id,所以需要對id進行加密,很想讓前端來做,可惜多個前端協調起來不方便(就是不想做),只能後端攻克一下了。
遇到問題肯定先看看有沒有前輩已經鋪好路了,找了一圈只發現Java語言實現的,參考連線如下:
需求分析
從業務需求上來看,生成碼有以下幾個強制性的要求:
- 不可重複
- 唯一確定
這兩點要求首先就排除了 hash code 的可能,因為 hash code 是可以發生碰撞的。然後在強制性要求的基礎之上,我們還有一些進一步的需求:
- 長度不能太長,6-10 位是合適的區間
- 不容易被推測出
- 資源消耗盡可能小
方案
閉眼寫系列
直接隨機生成,存資料庫,存之前做個校驗,但是這種方式會對資料庫進行多次訪問,無疑是對資源的過度佔用,顯然應該存在無需過資料直接在本地進行加密和解密的方法,如JWT。
其他排除的選項
列這一欄不過是提供一些其他的思路,或許可以用在其他地方。
UUID:太長(32個字元)!
時間戳:也很長,且深究起來可能會重複。當然某些場景會用到和時間相關的優化方案,如雪花演算法。
標識+隨機數:好想法,但是容易被反推出來。
最終方案
為了讓字母和數字的位置不固定,將ID 作 32 進位制轉換,即把 ID 對映為一串字母+數字的組合,高位用 0 補全。
同時把隨機數生成的範圍擴大到字母和數字,這樣密文中的每一位都可能是數字和字母,規律性就不易察覺得多。
然後是使用者 ID 暴露在密文中的問題。這個問題的解決辦法是我們可以加一點鹽
。鹽的取值最好不要太小,太小缺乏隱蔽性;也不能太大,太大會佔用過多使用者 ID 的取值空間。具體的取值取決於業務需求。
最後是校驗位的問題。固定在 2 位字元承擔起對密文其它部分的校驗功能。縮短後的校驗碼就沒有辦法隔位插入,我就把它放在了密文尾部。用這一套校驗方式,理論上能保證 99.9%的誤操作可以被後臺檢測出來而不需要查詢資料庫。
根據上方的邏輯生成的唯一碼仍然是有規律的,尤其是連續的id,有很多相似的地方,這是因為低位的變化不會影響高位,密碼學對此問題的解決方法是擴散和混淆。
擴散 (diffusion) 和混淆 (confusion) 是 C.E.Shannon 提出的設計密碼體制的兩種基本方法,其目的是為了抵抗對手對密碼體制的統計分析。在分組密碼的設計中,充分利用擴散和混淆,可以有效地抵抗對手從密文的統計特性推測明文或金鑰。擴散和混淆是現代分組密碼的設計基礎。
所謂擴散就是讓明文中的每一位影響密文中的許多位,或者說讓密文中的每一位受明文中的許多位的影響。這樣可以隱蔽明文的統計特性。當然,理想的情況是讓明文中的每一位影響密文中的所有位,或者說讓密文中的每一位受明文中所有位的影響。
所謂混淆就是將密文與金鑰之間的統計關係變得儘可能複雜,使得對手即使獲取了關於密文的一些統計特性,也無法推測金鑰。使用複雜的非線性代替變換可以達到比較好的混淆效果,而簡單的線性代替變換得到的混淆效果則不理想。可以用”揉麵團”來形象地比喻擴散和混淆。當然,這個”揉麵團”的過程應該是可逆的。乘積和迭代有助於實現擴散和混淆。選擇某些較簡單的受金鑰控制的密碼變換,通過乘積和迭代可以取得比較好的擴散和混淆的效果。
上面這些話都是我抄的,只怪當年沒好好聽密碼學這門課,我們還是直接看程式碼吧
程式碼實現
公共部分:
# 隨機字串,用於混淆
CHARS = ('F', 'L', 'G', 'W', '5', 'X', 'C', '3', '9', 'Z', 'M', '6', '7', 'Y', 'R', 'T', '2', 'H', 'S',
'8', 'D', 'V', 'E', 'J', '4', 'K', 'Q', 'P', 'U', 'A', 'N', 'B')
CHARS_LENGTH = 32
# 邀請碼長度
CODE_LENGTH = 8
# 隨機資料,加鹽
SALT = 131420
# 下方資料用於擴散
# PRIME1 與 CHARS 的長度 L互質,可保證 ( id * PRIME1) % L 在 [0,L)上均勻分佈
PRIME1 = 3
# PRIME2 與 CODE_LENGTH 互質,可保證 ( index * PRIME2) % CODE_LENGTH 在 [0,CODE_LENGTH)上均勻分佈
PRIME2 = 9
加密:
def encode(num: int) -> str:
# 擴散+加鹽
num = num * PRIME1 + SALT
# 下方為加密邏輯
b = [num] + [0 for _ in range(CODE_LEN - 1)]
for i in range(5):
b[i + 1] = b[i] // CHARS_LEN
b[i] = (b[i] + b[0] * i) % CHARS_LEN
# 最後兩位起到校驗的作用
b[5] = (b[0] + b[1] + b[2]) * PRIME1 % CHARS_LEN
b[6] = (b[3] + b[4] + b[5]) * PRIME1 % CHARS_LEN
code = ""
for i in range(CODE_LEN):
# 混淆的過程
code += CHARS[b[(i * PRIME2) % CODE_LEN]]
return code
解密:
def decode(code: str) -> int:
"""
對唯一碼的解碼,返回值-1代表驗證不通過
"""
# 長度校驗
if len(code) != CODE_LEN:
return -1
num = 0
a = [0 for _ in range(CODE_LEN)]
b = [0 for _ in range(CODE_LEN)]
# 反解的過程
for i in range(CODE_LEN):
a[(i * PRIME2) % CODE_LEN] = i
try:
for i in range(CODE_LEN):
a[i] = CHARS.index(code[a[i]])
except ValueError:
return -1
# 最後兩位起到校驗的作用,此處為校驗流程
b[5] = (a[0] + a[1] + a[2]) * PRIME1 % CHARS_LEN
b[6] = (a[3] + a[4] + a[5]) * PRIME1 % CHARS_LEN
if a[5] != b[5] or a[6] != b[6]:
return -1
# 反解num
for i in range(4, -1, -1):
b[i] = (a[i] - a[0] * i + CHARS_LEN * i) % CHARS_LEN
for i in range(4, 0, -1):
num = (num + b[i]) * CHARS_LEN
num = ((num + b[0]) - SALT) // PRIME1
return num
以上程式碼是經過本人精簡優化後,有些地方也仍然不是很理解,只能在繼續慢慢學習了,不過我還是更喜歡把程式碼封裝下,如下是封裝後的,使用上更方便。
class UniqCode:
"""
根據id生成字串形式的唯一碼,可用於邀請碼或分享碼的場景,加密與解密均在本地完成
"""
# 隨機字串,用於混淆
CHARS = ('F', 'L', 'G', 'W', '5', 'X', 'C', '3', '9', 'Z', 'M', '6', '7', 'Y', 'R', 'T', '2', 'H', 'S',
'8', 'D', 'V', 'E', 'J', '4', 'K', 'Q', 'P', 'U', 'A', 'N', 'B')
CHARS_LEN = 32
# 邀請碼長度,滿足大部分業務需求
CODE_LEN = 7
# 隨機資料,加鹽
SALT = 131420
# 下方資料用於擴散
# PRIME1 與 CHARS 的長度 L互質,可保證 ( id * PRIME1) % L 在 [0,L)上均勻分佈
PRIME1 = 3
# PRIME2 與 CODE_LENGTH 互質,可保證 ( index * PRIME2) % CODE_LENGTH 在 [0,CODE_LENGTH)上均勻分佈
PRIME2 = 9
def __new__(cls, *args, **kwargs):
"""設定單例模式"""
if not hasattr(cls, "_instance"):
cls._instance = super(UniqCode, cls).__new__(cls)
return cls._instance
@classmethod
def encode(cls, num: int) -> str:
# 擴散+加鹽
num = num * cls.PRIME1 + cls.SALT
# 下方為加密邏輯
b = [num] + [0 for _ in range(cls.CODE_LEN - 1)]
for i in range(5):
b[i + 1] = b[i] // cls.CHARS_LEN
b[i] = (b[i] + b[0] * i) % cls.CHARS_LEN
# 最後兩位起到校驗的作用
b[5] = (b[0] + b[1] + b[2]) * cls.PRIME1 % cls.CHARS_LEN
b[6] = (b[3] + b[4] + b[5]) * cls.PRIME1 % cls.CHARS_LEN
code = ""
for i in range(cls.CODE_LEN):
# 混淆的過程
code += cls.CHARS[b[(i * cls.PRIME2) % cls.CODE_LEN]]
return code
@classmethod
def decode(cls, code: str) -> int:
"""
對唯一碼的解碼,返回值-1代表驗證不通過
"""
# 長度校驗
if len(code) != cls.CODE_LEN:
return -1
num = 0
a = [0 for _ in range(cls.CODE_LEN)]
b = [0 for _ in range(cls.CODE_LEN)]
# 反解的過程
for i in range(cls.CODE_LEN):
a[(i * cls.PRIME2) % cls.CODE_LEN] = i
try:
for i in range(cls.CODE_LEN):
a[i] = cls.CHARS.index(code[a[i]])
except ValueError:
return -1
# 最後兩位起到校驗的作用,此處為校驗流程
b[5] = (a[0] + a[1] + a[2]) * cls.PRIME1 % cls.CHARS_LEN
b[6] = (a[3] + a[4] + a[5]) * cls.PRIME1 % cls.CHARS_LEN
if a[5] != b[5] or a[6] != b[6]:
return -1
# 反解num
for i in range(4, -1, -1):
b[i] = (a[i] - a[0] * i + cls.CHARS_LEN * i) % cls.CHARS_LEN
for i in range(4, 0, -1):
num = (num + b[i]) * cls.CHARS_LEN
num = ((num + b[0]) - cls.SALT) // cls.PRIME1
return num
給自己打個廣告,我是杜高強,一個在軟體行業摸爬打滾多年,一直在自學的程式猿,如果你也對這個行業感興趣,歡迎大家一起交流。