注册授权--续
由于上一篇文章中,把注册与授权全部代码都整合在一个项目中了。这样就会出现锁和钥匙都放在项目中。
如果打包给用户,反编译后就失去了加密的意义。于是下面做了整改。
=============================================================================
一、 管理员端
管理员端由两个独立的脚本组成,它们绝不能分发给最终用户。
1. 密钥对生成器 (generate_keys.py)
用途:生成整个授权体系的基石——公钥和私钥。此脚本只需在开发环境中运行一次。
# -*- coding: utf-8 -*- # 用途:生成 RSA 非对称加密密钥对 # 运行环境:管理员/开发者的电脑 # 运行次数:仅需一次 import os from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa # --- 生成密钥对 --- # 1. 生成一个 2048 位的 RSA 私钥。这是安全性和性能的平衡点。 private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) # 2. 从私钥中提取出公钥。 public_key = private_key.public_key() # --- 保存私钥 --- # 私钥必须由管理员绝对保密,用于签名激活码。 with open("private_key.pem", "wb") as f: f.write(private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption() # 不对文件本身加密,靠文件系统权限保护 )) print("✅ 私钥已保存到: private_key.pem (请绝对保密!)") # --- 保存公钥 --- # 公钥将被硬编码到客户端程序中,用于验证激活码。 with open("public_key.pem", "wb") as f: f.write(public_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo )) print("✅ 公钥已保存到: public_key.pem (将用于客户端)") print("\n🎉 密钥对生成完成!") print("下一步:") print("1. 将 public_key.pem 的内容复制到客户端代码中。") print("2. 使用 private_key.pem 运行 key_generator.py 来为用户生成激活码。")
2. 激活码生成器 (key_generator.py)
用途:使用私钥为指定机器码生成一个带签名的激活码。
# -*- coding: utf-8 -*- # 用途:为用户生成离线激活码 # 运行环境:管理员/开发者的电脑 # 依赖:必须与 private_key.pem 文件在同一目录下 import json import base64 from datetime import datetime, timedelta from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding # --- 加载私钥 --- # 从文件中加载 PEM 格式的私钥,这是签名激活码的关键。 try: with open("private_key.pem", "rb") as key_file: private_key = serialization.load_pem_private_key(key_file.read(), password=None) except FileNotFoundError: print("❌ 错误:找不到私钥文件 'private_key.pem'。请先运行 generate_keys.py。") exit() def create_license(machine_id: str, days: int) -> str: """ 使用私钥为指定的机器码和有效期创建一个激活码。 激活码格式:Base64编码的载荷 . Base64编码的签名 """ # 1. 创建授权信息载荷 expiry_date = (datetime.now() + timedelta(days=days)).isoformat(timespec='seconds') payload = { "mid": machine_id.upper(), # 绑定的机器码 "exp": expiry_date, # 过期时间 (ISO 8601 格式) } # 将 JSON 转换为紧凑的字节串 payload_bytes = json.dumps(payload, separators=(',', ':')).encode('utf-8') # 2. 使用私钥对载荷进行数字签名 # 签名可以确保数据在传输过程中未被篡改,且确实由私钥持有者(管理员)签发。 signature = private_key.sign( payload_bytes, padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), hashes.SHA256() ) # 3. 将载荷和签名都进行 Base64 编码,以便安全地作为文本传输 encoded_payload = base64.b64encode(payload_bytes).decode('utf-8') encoded_signature = base64.b64encode(signature).decode('utf-8') # 4. 用点号 '.' 连接,形成最终的激活码 return f"{encoded_payload}.{encoded_signature}" if __name__ == "__main__": print("========================================") print(" 激活码生成工具 (管理员专用)") print("========================================") # 获取用户输入 machine_id = input("\n请输入用户的机器码 (16位): ").strip().upper() if len(machine_id) != 16: print("⚠️ 警告:机器码必须是16位。") exit() days_str = input("请输入授权天数 (例如: 365): ").strip() days = int(days_str) if days_str.isdigit() else 365 # 生成激活码 license_key = create_license(machine_id, days) # 显示结果 print("\n" + "="*50) print("✅ 激活码生成成功!请复制给用户:") print("-" * 50) print(license_key) print("-" * 50) print(f"授权信息:") print(f" - 绑定机器码: {machine_id}") print(f" - 有效期: {days} 天") print("="*50)
二、 客户端
客户端代码需要打包分发给用户。核心是 license_manager.py,它只包含验证逻辑。
1. 授权核心模块 (license_manager.py)
用途:集成在您的软件中,负责所有客户端的授权验证工作。
# -*- coding: utf-8 -*- # 用途:软件注册授权核心模块 (客户端) # 功能:验证激活码、检查授权状态、获取授权信息 # 集成:此文件需要打包到您的最终软件中 import hashlib import uuid import json import os import base64 import platform import getpass from datetime import datetime from typing import Optional, Dict, Any from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding # --- 核心:公钥 --- # 【重要】请将管理员生成的 'public_key.pem' 文件内容完整复制到此处。 # 此公钥用于验证激活码,即使被反编译也无法用于生成新激活码。 PUBLIC_KEY_PEM = """ -----BEGIN PUBLIC KEY----- # <<< 请在这里粘贴您的公钥内容 >>> -----END PUBLIC KEY----- """ class LicenseManager: """客户端授权管理器""" LICENSE_FILE = "license.dat" # 存储已验证的激活码 def __init__(self): # 1. 加载公钥 self.public_key = serialization.load_pem_public_key(PUBLIC_KEY_PEM.strip().encode('utf-8')) # 2. 获取本机机器码 self.machine_id = self._get_machine_id() def _get_machine_id(self) -> str: """获取本机唯一的机器码""" # 组合 MAC 地址、用户名和主机名,提高唯一性 mac_str = f"{uuid.getnode():012X}" user = getpass.getuser() hostname = platform.node() raw = f"{mac_str}{user}{hostname}" # 生成16位哈希值作为机器码 return hashlib.sha256(raw.encode('utf-8')).hexdigest()[:16].upper() def activate(self, license_key: str) -> bool: """验证并激活软件""" try: # 1. 拆分激活码 encoded_payload, encoded_signature = license_key.strip().split('.') payload_bytes = base64.b64decode(encoded_payload) signature = base64.b64decode(encoded_signature) # 2. 使用公钥验证签名 # 这是核心安全步骤:确保数据来自管理员且未被篡改 self.public_key.verify( signature, payload_bytes, padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), hashes.SHA256() ) # 3. 解析数据 data = json.loads(payload_bytes.decode('utf-8')) # 4. 校验机器码是否匹配 if data.get("mid") != self.machine_id: print("错误:激活码与当前机器不匹配。") return False # 5. 校验是否过期 if datetime.now() > datetime.fromisoformat(data["exp"]): print("错误:激活码已过期。") return False # 6. 所有校验通过,保存激活码到本地 with open(self.LICENSE_FILE, 'w', encoding='utf-8') as f: f.write(license_key.strip()) print("激活成功!") return True except Exception as e: print(f"激活失败: {e}") return False def is_activated(self) -> bool: """检查是否已激活且有效""" if not os.path.exists(self.LICENSE_FILE): return False try: # 重新执行一次完整的验证流程,防止本地文件被篡改 with open(self.LICENSE_FILE, 'r', encoding='utf-8') as f: key = f.read().strip() return self.activate(key) except: return False def get_license_info(self) -> Dict[str, Any]: """获取授权详细信息,用于UI显示""" if not self.is_activated(): return {"status": "未激活"} try: with open(self.LICENSE_FILE, 'r', encoding='utf-8') as f: key = f.read().strip() parts = key.split('.') payload_bytes = base64.b64decode(parts[0]) data = json.loads(payload_bytes.decode('utf-8')) exp_date = datetime.fromisoformat(data["exp"]) return { "status": "已激活", "machine_id": self.machine_id, "expiry": exp_date.strftime('%Y-%m-%d %H:%M:%S'), "remaining_days": (exp_date - datetime.now()).days } except: return {"status": "授权异常"}
2. 程序入口与集成 (main.py)
用途:软件的启动文件,展示如何在程序启动时植入授权检查。
# -*- coding: utf-8 -*- # 用途:软件主程序入口 # 功能:在启动任何核心功能前,进行授权检查 import tkinter as tk from tkinter import simpledialog, messagebox from license_manager import LicenseManager def check_license_and_start(root: tk.Tk) -> bool: """ 授权检查函数(“守门员”) 返回 True: 授权通过,程序继续 返回 False: 授权失败,程序退出 """ lm = LicenseManager() # 1. 检查是否已激活 if not lm.is_activated(): # 2. 未激活,提示用户并显示机器码 messagebox.showinfo("软件未激活", f"本机机器码:\n{lm.machine_id}\n\n请联系管理员获取激活码。") # 3. 弹窗让用户输入激活码 license_key = simpledialog.askstring("软件激活", "请输入激活码:", parent=root) if license_key and lm.activate(license_key): messagebox.showinfo("成功", "激活成功!") return True else: messagebox.showerror("失败", "激活码无效或已过期。") return False else: # 4. 已激活,检查是否即将过期 info = lm.get_license_info() days = info.get("remaining_days") if days is not None and 0 < days <= 7: messagebox.showwarning("提醒", f"您的授权将在 {days} 天后过期,请及时续费!") return True # ========= 主程序入口 ========= if __name__ == "__main__": root = tk.Tk() root.title("我的软件") root.withdraw() # 先隐藏主窗口,避免闪烁 # 执行授权检查 if check_license_and_start(root): root.deiconify() # 授权通过,显示主窗口 # 在这里启动你的主程序界面... label = tk.Label(root, text="软件已成功启动!", font=("Arial", 20)) label.pack(pady=50) root.mainloop() else: root.destroy() # 授权失败,退出程序
完整工作流程与部署指南
第一阶段:管理员准备工作 (只需执行一次)
-
生成密钥对:
- 在您的开发电脑上,运行
generate_keys.py。 - 该脚本会生成两个文件:
private_key.pem和public_key.pem。 private_key.pem:这是您的最高机密,绝对不能泄露。它将用来为用户签名激活码。public_key.pem:这个是公钥,需要分发给所有客户端。
- 在您的开发电脑上,运行
-
配置客户端:
- 用文本编辑器打开
public_key.pem,复制其全部内容(包括-----BEGIN...和-----END...)。 - 打开客户端的
license_manager.py文件。 - 将复制的内容粘贴到
PUBLIC_KEY_PEM变量的多行字符串中,覆盖掉占位符。 - 保存
license_manager.py。现在,客户端已经内置了验证激活码所需的公钥。
- 用文本编辑器打开
-
准备分发:
- 将以下文件打包成您的软件安装包:
main.py(程序入口)license_manager.py(授权核心)- 您的其他业务代码文件 (如
main_window.py等)
- 注意:
private_key.pem和key_generator.py绝对不能放入分发包中!
- 将以下文件打包成您的软件安装包:
第二阶段:为用户生成激活码 (按需执行)
当有新用户需要授权时:
-
获取用户机器码:
- 用户运行您的软件,会看到一个“未激活”的弹窗,上面显示了他们电脑的16位机器码。
- 用户将此机器码发送给您。
-
生成激活码:
- 在您的开发电脑上,确保
private_key.pem文件和key_generator.py在同一个目录下。 - 运行
key_generator.py。 - 根据提示,输入用户的机器码和您想授权的天数(如 365)。
- 脚本会生成一长串激活码。
- 在您的开发电脑上,确保
-
交付激活码:
- 将生成的激活码复制并发送给用户。
第三阶段:用户激活与使用
-
输入激活码:
- 用户在软件的激活弹窗中,将您发给他们的激活码粘贴进去。
- 点击“确定”。
-
验证与激活:
- 客户端
license_manager.py开始工作:- 使用内置的公钥验证激活码的签名是否合法。
- 解码激活码内容,检查绑定的机器码是否与当前电脑一致。
- 检查激活码是否已过期。
- 如果所有检查都通过,激活成功,激活码会被保存到
license.dat文件中,主程序界面显示。
- 客户端
-
后续使用:
- 用户每次启动软件,程序都会自动读取
license.dat并重新验证,无需再次输入激活码。 - 如果用户将软件拷贝到其他电脑,由于机器码不匹配,激活会失败。
- 如果授权即将到期,程序会提前提醒用户续费。
- 用户每次启动软件,程序都会自动读取
总结与最佳实践
- 安全性:这套方案的核心安全在于非对称加密和职责分离。私钥在您手中,公钥在客户端,攻击者无法伪造有效激活码。
- 代码混淆:为了进一步提高安全性,防止攻击者通过反编译
.pyc文件来绕过授权检查,建议在打包分发前使用代码混淆工具(如PyArmor)对您的 Python 代码进行加密。 - 文件完整性:
license.dat文件是授权状态的唯一凭证。虽然客户端每次都会重新验证,但也可以考虑对该文件本身进行一次简单的校验和(checksum)检查,以防止被粗暴地篡改。

浙公网安备 33010602011771号