大家好,我是机灵鹤。
之前写了一篇关于软件授权码机制的文章,教大家如何给软件添加授权码机制,避免自己写的程序被别人随意传播,收到了不少读者朋友的好评。
本文将在之前版本的基础上进一步优化,给授权码增加有效期限制,即有效期内可以使用,过期则失效。实现授权码机制 V2.0
版本。
1. 总体思路
此版本的授权机制在总体思路上与前一版本相似,在授权流程的细节方面做了一些优化。
1.1 授权码生成
在生成授权码时,加入授权有效期的限制。
在上一个版本中,授权码是使用加密算法对 机器码
直接加密后生成。
此版本中,授权码由两个部分构成,第一,获取机器码,将机器码加密生成 激活码
;第二,获取授权截止时间,将时间转换为 时间戳
。然后通过自定义的规则将 激活码
和 时间戳
组合成一个新的字符串,最后对该字符串进行加密处理,得到最终的授权码。
如图所示,是授权码生成机制的流程图。
1.2 授权码校验
在校验授权码时,加入授权有效期的判断。
在上一个版本的授权码校验中,程序内部首先执行一次加密算法,得到一个 校验码
,若输入的 授权码
与 校验码
一致,则验证通过,否则验证失败。
在此版本中,使用的是校验码和授权有效期的双重验证。
首先对授权码进行解密,提取出 激活码
和 有效期
;然后将 激活码
与程序内部计算得到的 校验码
进行比较,将 有效期限
与 当前时间
进行比较。当激活码与校验码一致,且当前时间在有效期内,则验证通过,否则验证失败。
如图所示, 是授权码校验机制的流程图。
2. 实现步骤
由于此版本的代码主体沿用上个版本,所以其中相同的步骤,如获取机器信息,生成机器码等,暂且跳过,这里着重讲解优化的部分。
2.1 实现加密解密算法
由于需要对授权数据进行加密和解密,兼顾安全性和性能,我这里选择使用 AES
算法 - CBC
模式。
为了方便使用,这里封装一个自己的类,用于数据加密和解密。
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
class My_AES_CBC():
def __init__(self, key, iv):
# key 和 iv 必须为 16 位
self.key = key
self.mode = AES.MODE_CBC
self.cryptor = AES.new(self.key, self.mode, iv)
def encrypt(self, plain_text):
encode_text = plain_text.encode('utf-8')
pad_text = pad(encode_text, AES.block_size)
encrypted_text = self.cryptor.encrypt(pad_text)
return encrypted_text
def decrypt(self, encrypted_text):
plain_text = self.cryptor.decrypt(encrypted_text)
plain_text = unpad(plain_text, AES.block_size).decode()
return plain_text
简单测试一下加密解密功能:
if __name__ == '__main__':
key = '9B8FD68A366F4D03'.encode()
iv = '305FB72D83134CA0'.encode('utf-8')
text = "大家好,我是机灵鹤,Hello World!"
e = My_AES_CBC(key, iv).encrypt(text)
print(e)
d = My_AES_CBC(key, iv).decrypt(e)
print(d)
运行结果:
b'\x12t#\xcd\x97\x7f\xb5\xaaL\x9a\xac\x1e\xee"\xcf_\xdd\xcf\xac\x15\xef\xbc\xfd\x04\xae\x0e\xae\xedg\x80\xabr\xcb\xe3P\x86\x01)\x9d,\xbb\xb0\x10\xe7\xbf\xf4(Y'
大家好,我是机灵鹤,Hello World!
成功实现了对字符串的加密和解密。
2.2 生成授权码
授权码的生成方法其实并不固定,每个人都可以研究一套自己独有的加密方法。
只需要满足 授权码生成时的加密过程
与 授权码校验时的解密过程
中 算法
和 参数
一致即可。
作为示例,我演示一下我 Demo 中的加密方法。
# aes 加密用到的 key 和 iv,长度必须为 16 位,随机字符串
Aes_key = '9B8FD68A366F4D03'.encode()
Aes_IV = '305FB72D83134CA0'.encode('utf-8')
def getActiveCode(machine_code):
"""
用于通过机器码,生成激活码
machine_code: 机器码
"""
encrypt_code = My_AES_CBC(Aes_key, Aes_IV).encrypt(machine_code)
active_code = hashlib.md5(encrypt_code).hexdigest().upper()
return active_code
def getTimeLimitedCode(machine_code, timestamp):
"""
用于通过机器码和有效期时间戳,生成限时授权码
machine_code: 机器码
timestamp: 有效期的时间戳
"""
# 生成激活码
active_code = getActiveCode(machine_code)
# 组合成 json 格式,并转换成字符串
data = {
"code": active_code,
"endTs": timestamp,
}
text = json.dumps(data);
# AES 加密
encrypt_code = My_AES_CBC(Aes_key, Aes_IV).encrypt(text)
# base64 加密,生成授权码
active_code = base64.b32encode(encrypt_code)
return active_code.decode()
简单测试一下限时授权码的生成功能,
if __name__ == '__main__':
# 机器码
machine_code = "7BA4E7D187C65A2EBE993C8257743487"
# 时间戳
time_array = time.strptime("2023-05-20 12:00:00", '%Y-%m-%d %H:%M:%S')
timestamp = int(time.mktime(time_array))
# 授权码
code = getTimeLimitedCode(machine_code, timestamp)
print(code)
运行结果:
MCDJE3GV5C23LWNNO7WQYEOZ5SW4CKSG5M6JOJVXJNL5CRBNEWMOQKR3FIFVGTQVIDB7WWHLK47DASW5VNUJBR4C7KTQMKRBPIG6H3ASIUT6SHNIE7KJXU7XDY6QZELO
成功生成了授权码字符串。
2.3 授权验证逻辑
授权码验证环节的解密算法,其实就是对授权码生成时的加密算法的逆运算。
将 激活码
和 有效期至
解析出来以后进行校验,即可完成授权码验证。
下面是我 demo 中的授权验证逻辑。
# AES_CBC 加密
def Encrypt(plain_text):
e = My_AES_CBC(Aes_key, Aes_IV).encrypt(plain_text)
return e
# AES_CBC 解密
def Decrypt(encrypted_text):
d = My_AES_CBC(Aes_key, Aes_IV).decrypt(encrypted_text)
return d
def checkKeyCode(machine_code, key_code):
# 授权码解密,提取出激活码和有效期
register_str = base64.b32decode(key_code)
decode_key_data = json.loads(Decrypt(register_str))
active_code = decode_key_data["code"].upper() # 激活码
end_timestamp = decode_key_data["endTs"] # 有效期
# 加密机器码,用于跟激活码对比
encrypt_code = Encrypt(machine_code)
md5_code = hashlib.md5(encrypt_code).hexdigest().upper()
# 获取本地时间,用于跟有效期对比
curTs = int(time.time())
if md5_code != active_code:
print("激活码错误,请重新输入!")
elif curTs >= end_timestamp:
print("激活码已过期,请重新输入!")
else:
time_local = time.localtime(end_timestamp)
dt = time.strftime("%Y-%m-%d %H:%M:%S", time_local)
print("激活成功!有效期至 %s" %dt)
将刚才使用的机器码和生成的授权码输入进去,验证一下,
if __name__ == '__main__':
# 机器码
machine_code = "7BA4E7D187C65A2EBE993C8257743487"
# 授权码
key_code = "MCDJE3GV5C23LWNNO7WQYEOZ5SW4CKSG5M6JOJVXJNL5CRBNEWMOQKR3FIFVGTQVIDB7WWHLK47DASW5VNUJBR4C7KTQMKRBPIG6H3ASIUT6SHNIE7KJXU7XDY6QZELO"
# 授权校验
checkKeyCode(machine_code, key_code)
运行结果:
激活成功!有效期至 2023-05-20 12:00:00
2.4 打包测试
项目完成以后,打包可以使用 pyinstaller
库。
没有安装 pyinstaller
库的,可以运行下面的命令行来安装。
pip install pyinstaller
使用 pyinstaller
库打包的命令如下:
pyinstaller -F xxx.py
打包完成以后,会在当前目录下的 dist
文件夹中,生成 xxx.exe
可执行程序。
3. 效果展示
3.1 运行效果
首次启动程序时(无授权文件),会提示输入激活码,即授权码。
随便输入错误的授权码,会验证失败,提示重新输入。
此时我们启动注册机,根据提示复制机器码 759C7E7B1776F78ED3AC317A8142F02C
到注册机中,并设置授权到期时间,生成限时激活码。
复制并输入激活码,此时授权验证成功,程序可以正常使用,输出了 Hello World!
字样。
后续再次启动程序时,由于已有授权文件,所以可以直接进入。
当时间超出授权有效期时,启动程序会提示 激活码失效
,需要重新获取激活码进行激活。
而此时如果输入之前获取的激活码,会提示 激活码已过期
。
3.2 源码分享
为了方便大家学习交流,我将代码整理打包上传,并附上了一个 Demo 程序。
关注公众号
机灵鹤
并回复文字授权码
,即可获取。
感兴趣的同学可以自行下载,大家一起交流学习。
4. 优化和展望
4.1 细节优化
以下是根据大家的使用反馈,做的一些功能优化和 bug 修改。
4.1.1 重启电脑机器码会变化
有读者反馈,自己重启电脑后,机器码会发生变化。
机器码是根据硬件的序列号生成的,理论上来讲,只要不更换硬件,机器码都是不变的。
排查以后发现,问题出在下面这段代码。
# 硬盘序列号 15 位
def get_disk_serial(self):
disk_info = self.m_wmi.Win32_PhysicalMedia()
if len(disk_info) > 0:
serial_number = disk_info[0].SerialNumber.strip()
return serial_number
else:
return "WD-ABCDEFGHIJKL"
代码的逻辑为,获取机器中的所有硬盘数据,然后返回第一个硬盘的序列号。
关键问题在于,当机器中有多个硬盘的时候, disk_info
中硬盘数据的顺序是随机的,这就导致取到的第一个硬盘的序列号会发生变化。
# 硬盘序列号 15 位
def get_disk_serial(self):
disk_info = self.m_wmi.Win32_PhysicalMedia()
disk_info.sort() # 排序
if len(disk_info) > 0:
serial_number = disk_info[0].SerialNumber.strip()
return serial_number
else:
return "WD-ABCDEFGHIJKL"
我们先对 disk_info
进行一次排序,就可以保证每次取到的序列号是一致的。
4.1.2 授权文件中明文存储
上个版本的本地授权文件 register.bin
中的数据是通过明文存储的,别人可以直接打开看到里面的内容,一眼便知道这个文件是授权文件,因为里面明文存放着授权码。
在此版本中,本地授权文件优化为存储加密后的 二进制数据
,这样直接打开时显示的是乱码。
如果你再给文件改个名,别人不一定能认得出这个就是授权文件,一定程度上提高了数据的安全性。
4.2 展望
作为 V2.0
版本的授权码机制,虽然加入了授权有效期的机制,实现了更加精细的授权控制,但毕竟是离线的授权机制,在安全性和授权管理方面还是不够好。
所以,如果还有下个版本,可以考虑将授权方式改成在线验证。
如果文章中有哪里没有讲明白,或者讲解有误的地方,欢迎在评论区批评指正,或者扫描下面的二维码,加我微信,大家一起学习交流,共同进步。
此处评论已关闭