编辑
2026-04-29
Python
00

目录

CustomTkinter软件授权系统实战:从零搭建简单防盗版机制
🔐 当你的软件被随意复制,你该怎么办?
🧠 先想清楚:授权系统的本质是什么
🛠️ 环境准备
⚙️ 核心模块:机器码与激活码
📌 采集机器特征,生成机器码
🔑 生成激活码
✅ 本地验证逻辑
🎨 界面部分:CustomTkinter激活窗口
🚀 主程序集成
🔑 Key保存到本地
⚠️ 几个必须注意的坑
💬 写在最后

CustomTkinter软件授权系统实战:从零搭建简单防盗版机制


🔐 当你的软件被随意复制,你该怎么办?

做过桌面工具的开发者,大概都遇到过这种情况——花了几个月心血写出来的小工具,发出去没多久就被人打包转发,甚至有人直接拿去卖钱。气不气?当然气。但更现实的问题是:怎么防?

完全防住是不可能的,这一点咱们得想开。逆向工程、内存dump、代码混淆绕过……高手面前没有铜墙铁壁。但我们的目标不是对抗顶级黑客,而是提高普通用户随意传播的门槛,让"复制粘贴就能用"这条路走不通。

本文要做的事很具体:用 CustomTkinter 搭一套基于机器码绑定的本地授权系统,包含激活码生成、验证、界面集成的完整流程。代码全部可以直接跑,没有废话。


🧠 先想清楚:授权系统的本质是什么

很多人一上来就问"用什么加密算法",其实这个问题排在第二位。第一位的问题是:你的授权凭证跟什么绑定?

常见的绑定维度有三种:

账号绑定——需要联网验证,服务器说你有权限你才有。灵活,但需要维护后端,断网就凉。

设备绑定——把激活码跟某台机器的硬件特征挂钩,换台电脑就失效。离线可用,实现相对简单。

时间绑定——设置有效期,到期自动失效。通常和前两种结合使用。

咱们今天做的是设备绑定 + 本地验证的方案。核心逻辑是这样的:

用户机器 → 采集硬件特征 → 生成机器码 开发者拿到机器码 → 用密钥生成激活码 用户输入激活码 → 本地验证 → 解锁功能

没有服务器,没有联网请求,所有验证在本地完成。简单、稳定,适合个人开发者和小团队。


🛠️ 环境准备

依赖不多,几行装完:

bash
pip install customtkinter pip install cryptography

cryptography 库负责加解密,customtkinter 负责界面。Python版本建议 3.9 及以上。

项目结构规划如下:

license_demo/ ├── main.py # 主程序入口 ├── license_core.py # 授权核心逻辑 ├── ui_activate.py # 激活界面 └── keygen.py # 开发者用的激活码生成工具

⚙️ 核心模块:机器码与激活码

📌 采集机器特征,生成机器码

机器码的质量决定了绑定的精确度。采集太多硬件特征,用户换个硬盘就失效,体验很差;采集太少,换台配置相同的机器可能就绕过去了。

我的经验是取 CPU ID + 主板序列号 这两个维度,稳定性和唯一性之间的平衡还不错。

python
# license_core.py import hashlib import subprocess import platform import hmac import base64 from pathlib import Path LICENSE_FILE = Path.home() / ".myapp_license" SECRET_KEY = b"your-secret-key-change-this-2024" # 实际使用时换成随机长字符串 def _get_cpu_id() -> str: """获取CPU标识,跨平台处理""" try: if platform.system() == "Windows": result = subprocess.check_output( "wmic cpu get ProcessorId", shell=True ).decode() return result.strip().split("\n")[-1].strip() except Exception: pass return "UNKNOWN_CPU" def _get_board_serial() -> str: """获取主板序列号""" try: if platform.system() == "Windows": result = subprocess.check_output( "wmic baseboard get SerialNumber", shell=True ).decode() return result.strip().split("\n")[-1].strip() except Exception: pass return "UNKNOWN_BOARD" def get_machine_code() -> str: """ 生成当前机器的唯一标识码 取CPU和主板序列号拼接后做SHA256,截取前16位 """ raw = _get_cpu_id() + "|" + _get_board_serial() digest = hashlib.sha256(raw.encode()).hexdigest() # 格式化成 XXXX-XXXX-XXXX-XXXX 方便用户看 code = digest[:16].upper() return "-".join([code[i:i+4] for i in range(0, 16, 4)])

跑一下看看效果,你会得到类似 A3F2-9C1D-B7E4-0082 这样的字符串。每台机器不一样,同一台机器每次运行结果相同——这就是我们要的。

🔑 生成激活码

开发者拿到用户的机器码之后,用密钥生成对应的激活码。这里用 HMAC-SHA256,把机器码作为消息,密钥是我们自己保管的那串字符。

python
def generate_license_key(machine_code: str) -> str: """ 根据机器码生成激活码(开发者端使用) machine_code: 用户提供的机器码,如 A3F2-9C1D-B7E4-0082 """ clean_code = machine_code.replace("-", "").upper() signature = hmac.new( SECRET_KEY, clean_code.encode(), hashlib.sha256 ).digest() # Base64编码后取前24个字符,格式化为6段 b64 = base64.b32encode(signature).decode()[:24] return "-".join([b64[i:i+4] for i in range(0, 24, 4)])

这个函数放在 keygen.py 里,只在你自己的机器上运行,绝对不要打包进发布版本。

✅ 本地验证逻辑

用户输入激活码后,程序重新计算一遍"如果这台机器的机器码对应的激活码应该是什么",然后和用户输入的做比对。匹配就通过,不匹配就拒绝。

python
def verify_license(license_key: str) -> bool: """验证激活码是否与当前机器匹配""" machine_code = get_machine_code() expected = generate_license_key(machine_code) # 用 hmac.compare_digest 防止时序攻击 return hmac.compare_digest( license_key.strip().upper(), expected.upper() ) def save_license(license_key: str) -> None: """将激活码保存到用户目录""" LICENSE_FILE.write_text(license_key.strip()) def load_license() -> str: """从文件读取已保存的激活码""" if LICENSE_FILE.exists(): return LICENSE_FILE.read_text().strip() return "" def is_activated() -> bool: """检查当前机器是否已激活""" saved_key = load_license() if not saved_key: return False return verify_license(saved_key)

注意 hmac.compare_digest 这个细节——它保证比较时间恒定,防止通过响应时间差来猜测激活码。这种攻击在本地场景下威胁不大,但养成好习惯没坏处。


🎨 界面部分:CustomTkinter激活窗口

核心逻辑有了,现在给它穿件衣服。

python
# ui_activate.py import customtkinter as ctk from license_core import get_machine_code, verify_license, save_license ctk.set_appearance_mode("dark") ctk.set_default_color_theme("blue") class ActivationWindow(ctk.CTk): def __init__(self): super().__init__() self.title("软件激活") self.geometry("480x320") self.resizable(False, False) self._build_ui() def _build_ui(self): # 标题 ctk.CTkLabel( self, text="软件激活", font=ctk.CTkFont(size=22, weight="bold") ).pack(pady=(30, 5)) ctk.CTkLabel( self, text="请将以下机器码发送给开发者获取激活码", font=ctk.CTkFont(size=13), text_color="gray" ).pack() # 机器码展示区 machine_frame = ctk.CTkFrame(self, fg_color="transparent") machine_frame.pack(pady=15, padx=40, fill="x") self.machine_code = get_machine_code() code_entry = ctk.CTkEntry( machine_frame, width=300, font=ctk.CTkFont(size=14, family="Consolas"), state="readonly" ) code_entry.pack(side="left", expand=True, fill="x") code_entry.insert(0, self.machine_code) ctk.CTkButton( machine_frame, text="复制", width=60, command=lambda: self._copy_to_clipboard(self.machine_code) ).pack(side="left", padx=(8, 0)) # 激活码输入区 ctk.CTkLabel(self, text="激活码:").pack(anchor="w", padx=40) self.key_entry = ctk.CTkEntry( self, width=400, font=ctk.CTkFont(size=13, family="Consolas"), placeholder_text="XXXX-XXXX-XXXX-XXXX-XXXX-XXXX" ) self.key_entry.pack(padx=40, pady=(4, 0)) # 状态提示 self.status_label = ctk.CTkLabel( self, text="", font=ctk.CTkFont(size=12) ) self.status_label.pack(pady=8) # 激活按钮 ctk.CTkButton( self, text="立即激活", width=200, command=self._do_activate ).pack(pady=(0, 20)) def _copy_to_clipboard(self, text: str): self.clipboard_clear() self.clipboard_append(text) self.status_label.configure(text="机器码已复制", text_color="#4CAF50") def _do_activate(self): key = self.key_entry.get().strip() if not key: self.status_label.configure(text="请输入激活码", text_color="#FF5722") return if verify_license(key): save_license(key) self.status_label.configure( text="激活成功!程序将在3秒后重启", text_color="#4CAF50" ) self.after(3000, self._restart_app) else: self.status_label.configure( text="激活码无效,请检查后重试", text_color="#FF5722" ) def _restart_app(self): self.destroy() # 实际项目里这里重新启动主窗口 import main main.launch() if __name__ == "__main__": app = ActivationWindow() app.mainloop()

界面长这样:上半部分展示机器码(带一键复制),下半部分让用户输入激活码,点击按钮验证。简洁,够用。


🚀 主程序集成

把授权检查嵌入主程序入口,是整个流程的最后一步。

python
# main.py import customtkinter as ctk from license_core import is_activated ctk.set_appearance_mode("dark") ctk.set_default_color_theme("blue") class MainApp(ctk.CTk): def __init__(self): super().__init__() self.title("我的软件 - 已激活") self.geometry("800x500") ctk.CTkLabel( self, text="主功能界面", font=ctk.CTkFont(size=28, weight="bold") ).pack(expand=True) def launch(): if not is_activated(): # 未激活,跳转激活窗口 from ui_activate import ActivationWindow app = ActivationWindow() app.mainloop() else: app = MainApp() app.mainloop() if __name__ == "__main__": launch()

image.png

image.png

逻辑很直白:启动时先检查激活状态,没激活就弹激活窗口,激活了才进主界面。

🔑 Key保存到本地

python
def save_license(license_key: str) -> None: """ 激活成功后将激活码持久化到本地文件 下次启动直接读取,无需重新输入 """ LICENSE_FILE.write_text(license_key.strip(), encoding="utf-8") def load_license() -> str: """读取已保存的激活码,文件不存在则返回空字符串""" if LICENSE_FILE.exists(): return LICENSE_FILE.read_text(encoding="utf-8").strip() return "" def is_activated() -> bool: """ 启动时调用此函数: 1. 读取本地授权文件 2. 用当前机器码重新验证 3. 两者匹配则直接放行,无需用户操作 """ saved_key = load_license() if not saved_key: return False return verify_license(saved_key)

⚠️ 几个必须注意的坑

密钥泄露是最大风险。 SECRET_KEY 一旦被逆向工程提取,整套系统就形同虚设。建议用 PyInstaller 打包时配合 --key 参数做字节码加密,同时对关键字符串做简单混淆处理,提高提取难度。

虚拟机环境的硬件特征可能不稳定。 有些虚拟机的CPU ID和主板序列号每次启动都不一样,或者返回全零。可以在 get_machine_code() 里加一个兜底策略,当硬件信息明显异常时,混入网卡MAC地址作为补充维度。

授权文件被手动删除后需要重新激活。 这是正常现象,但如果用户频繁重装系统,体验会很差。可以考虑把授权文件存到注册表(Windows),或者用 winreg 模块写入系统级位置,增加被意外删除的难度。

不要在界面上暴露太多调试信息。 错误提示只说"激活码无效",不要说"机器码不匹配"或者"HMAC验证失败"——这些细节会给逆向分析者提供线索。


💬 写在最后

这套方案的强度,定位很清楚:挡住普通用户,不挡专业逆向。如果你的软件面对的是企业客户或者有一定付费意愿的专业用户,这个级别的保护已经够用了。

真正想深入这个方向的话,下一步可以研究:联网验证 + 本地缓存的混合模式、代码混淆工具(如 pyarmor)的配合使用,以及把激活逻辑放进C扩展模块来增加分析难度。

欢迎在评论区聊聊你在软件分发中遇到的实际问题,或者分享你用过的其他授权方案。


#Python开发 #CustomTkinter #软件授权 #桌面应用 #防盗版

本文作者:技术老小子

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!