大家好,我是机灵鹤。

之前写了一篇关于软件授权码机制的文章,教大家如何给软件添加授权码机制,避免自己写的程序被别人随意传播,收到了不少读者朋友的好评。

如何用 Python 优雅地给程序添加授权码机制

本文将在之前版本的基础上进一步优化,给授权码增加有效期限制,即有效期内可以使用,过期则失效。实现授权码机制 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 版本的授权码机制,虽然加入了授权有效期的机制,实现了更加精细的授权控制,但毕竟是离线的授权机制,在安全性和授权管理方面还是不够好。

所以,如果还有下个版本,可以考虑将授权方式改成在线验证。


如果文章中有哪里没有讲明白,或者讲解有误的地方,欢迎在评论区批评指正,或者扫描下面的二维码,加我微信,大家一起学习交流,共同进步。

最后修改:2023 年 04 月 24 日 10 : 17 AM
如果觉得我的文章对你有用,请随意赞赏