Python 根据id生成唯一码
admin
撰写于 2022年 01月 22 日

Python 根据id生成唯一码

最近业务中遇到需要分享某个文案,复制文案打开APP需要提取文案中包含的id,但又不想明文暴露id,所以需要对id进行加密,很想让前端来做,可惜多个前端协调起来不方便(就是不想做),只能后端攻克一下了。

遇到问题肯定先看看有没有前辈已经铺好路了,找了一圈只发现Java语言实现的,参考连接如下:

简单的密码学生成唯一邀请码

基于全局ID生成全局唯一邀请码

需求分析

从业务需求上来看,生成码有以下几个强制性的要求:

  • 不可重复
  • 唯一确定

这两点要求首先就排除了 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

给自己打个广告,我是杜高强,一个在软件行业摸爬打滚多年,一直在自学的程序猿,如果你也对这个行业感兴趣,欢迎大家一起交流。

Python 根据id生成唯一码的

Python 根据id生成唯一码

Python 根据id生成唯一码

最近业务中遇到需要分享某个文案,复制文案打开APP需要提取文案中包含的id,但又不想明文暴露id,所以需要对id进行加密,很想让前端来做,可惜多个前端协调起来不方便(就是不想做),只能后端攻克一下了。

遇到问题肯定先看看有没有前辈已经铺好路了,找了一圈只发现Java语言实现的,参考连接如下:

简单的密码学生成唯一邀请码

基于全局ID生成全局唯一邀请码

需求分析

从业务需求上来看,生成码有以下几个强制性的要求:

  • 不可重复
  • 唯一确定

这两点要求首先就排除了 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

给自己打个广告,我是杜高强,一个在软件行业摸爬打滚多年,一直在自学的程序猿,如果你也对这个行业感兴趣,欢迎大家一起交流。

Python 根据id生成唯一码的

赞 (0)

猜您想看

评论区(暂无评论)

这里空空如也,快来评论吧~

我要评论