🔐 当你的软件被随意复制,你该怎么办?
做过桌面工具的开发者,大概都遇到过这种情况——花了几个月心血写出来的小工具,发出去没多久就被人打包转发,甚至有人直接拿去卖钱。气不气?当然气。但更现实的问题是:怎么防?
完全防住是不可能的,这一点咱们得想开。逆向工程、内存dump、代码混淆绕过……高手面前没有铜墙铁壁。但我们的目标不是对抗顶级黑客,而是提高普通用户随意传播的门槛,让"复制粘贴就能用"这条路走不通。
本文要做的事很具体:用 CustomTkinter 搭一套基于机器码绑定的本地授权系统,包含激活码生成、验证、界面集成的完整流程。代码全部可以直接跑,没有废话。
🧠 先想清楚:授权系统的本质是什么
很多人一上来就问"用什么加密算法",其实这个问题排在第二位。第一位的问题是:你的授权凭证跟什么绑定?
常见的绑定维度有三种:
账号绑定——需要联网验证,服务器说你有权限你才有。灵活,但需要维护后端,断网就凉。
设备绑定——把激活码跟某台机器的硬件特征挂钩,换台电脑就失效。离线可用,实现相对简单。
时间绑定——设置有效期,到期自动失效。通常和前两种结合使用。
咱们今天做的是设备绑定 + 本地验证的方案。核心逻辑是这样的:
1用户机器 → 采集硬件特征 → 生成机器码
2开发者拿到机器码 → 用密钥生成激活码
3用户输入激活码 → 本地验证 → 解锁功能没有服务器,没有联网请求,所有验证在本地完成。简单、稳定,适合个人开发者和小团队。
🛠️ 环境准备
依赖不多,几行装完:
bash1pip install customtkinter
2pip install cryptographycryptography 库负责加解密,customtkinter 负责界面。Python版本建议 3.9 及以上。
项目结构规划如下:
1license_demo/
2├── main.py # 主程序入口
3├── license_core.py # 授权核心逻辑
4├── ui_activate.py # 激活界面
5└── keygen.py # 开发者用的激活码生成工具⚙️ 核心模块:机器码与激活码
📌 采集机器特征,生成机器码
机器码的质量决定了绑定的精确度。采集太多硬件特征,用户换个硬盘就失效,体验很差;采集太少,换台配置相同的机器可能就绕过去了。
我的经验是取 CPU ID + 主板序列号 这两个维度,稳定性和唯一性之间的平衡还不错。
python1# license_core.py
2import hashlib
3import subprocess
4import platform
5import hmac
6import base64
7from pathlib import Path
8
9LICENSE_FILE = Path.home() / ".myapp_license"
10SECRET_KEY = b"your-secret-key-change-this-2024" # 实际使用时换成随机长字符串
11
12
13def _get_cpu_id() -> str:
14"""获取CPU标识,跨平台处理"""
15try:
16if platform.system() == "Windows":
17 result = subprocess.check_output(
18"wmic cpu get ProcessorId", shell=True
19 ).decode()
20return result.strip().split("\n")[-1].strip()
21except Exception:
22pass
23return "UNKNOWN_CPU"
24
25
26def _get_board_serial() -> str:
27"""获取主板序列号"""
28try:
29if platform.system() == "Windows":
30 result = subprocess.check_output(
31"wmic baseboard get SerialNumber", shell=True
32 ).decode()
33return result.strip().split("\n")[-1].strip()
34except Exception:
35pass
36return "UNKNOWN_BOARD"
37
38
39def get_machine_code() -> str:
40"""
41 生成当前机器的唯一标识码
42 取CPU和主板序列号拼接后做SHA256,截取前16位
43 """
44 raw = _get_cpu_id() + "|" + _get_board_serial()
45 digest = hashlib.sha256(raw.encode()).hexdigest()
46# 格式化成 XXXX-XXXX-XXXX-XXXX 方便用户看
47 code = digest[:16].upper()
48return "-".join([code[i:i+4] for i in range(0, 16, 4)])跑一下看看效果,你会得到类似 A3F2-9C1D-B7E4-0082 这样的字符串。每台机器不一样,同一台机器每次运行结果相同——这就是我们要的。
🔑 生成激活码
开发者拿到用户的机器码之后,用密钥生成对应的激活码。这里用 HMAC-SHA256,把机器码作为消息,密钥是我们自己保管的那串字符。
python1def generate_license_key(machine_code: str) -> str:
2"""
3 根据机器码生成激活码(开发者端使用)
4 machine_code: 用户提供的机器码,如 A3F2-9C1D-B7E4-0082
5 """
6 clean_code = machine_code.replace("-", "").upper()
7 signature = hmac.new(
8SECRET_KEY,
9 clean_code.encode(),
10 hashlib.sha256
11 ).digest()
12# Base64编码后取前24个字符,格式化为6段
13 b64 = base64.b32encode(signature).decode()[:24]
14return "-".join([b64[i:i+4] for i in range(0, 24, 4)])这个函数放在 keygen.py 里,只在你自己的机器上运行,绝对不要打包进发布版本。
✅ 本地验证逻辑
用户输入激活码后,程序重新计算一遍"如果这台机器的机器码对应的激活码应该是什么",然后和用户输入的做比对。匹配就通过,不匹配就拒绝。
python1def verify_license(license_key: str) -> bool:
2"""验证激活码是否与当前机器匹配"""
3 machine_code = get_machine_code()
4 expected = generate_license_key(machine_code)
5# 用 hmac.compare_digest 防止时序攻击
6return hmac.compare_digest(
7 license_key.strip().upper(),
8 expected.upper()
9 )
10
11
12def save_license(license_key: str) -> None:
13"""将激活码保存到用户目录"""
14LICENSE_FILE.write_text(license_key.strip())
15
16
17def load_license() -> str:
18"""从文件读取已保存的激活码"""
19if LICENSE_FILE.exists():
20return LICENSE_FILE.read_text().strip()
21return ""
22
23
24def is_activated() -> bool:
25"""检查当前机器是否已激活"""
26 saved_key = load_license()
27if not saved_key:
28return False
29return verify_license(saved_key)注意 hmac.compare_digest 这个细节——它保证比较时间恒定,防止通过响应时间差来猜测激活码。这种攻击在本地场景下威胁不大,但养成好习惯没坏处。
🎨 界面部分:CustomTkinter激活窗口
核心逻辑有了,现在给它穿件衣服。
python1# ui_activate.py
2import customtkinter as ctk
3from license_core import get_machine_code, verify_license, save_license
4
5ctk.set_appearance_mode("dark")
6ctk.set_default_color_theme("blue")
7
8
9class ActivationWindow(ctk.CTk):
10def __init__(self):
11super().__init__()
12 self.title("软件激活")
13 self.geometry("480x320")
14 self.resizable(False, False)
15 self._build_ui()
16
17def _build_ui(self):
18# 标题
19 ctk.CTkLabel(
20 self, text="软件激活", font=ctk.CTkFont(size=22, weight="bold")
21 ).pack(pady=(30, 5))
22
23 ctk.CTkLabel(
24 self, text="请将以下机器码发送给开发者获取激活码",
25 font=ctk.CTkFont(size=13), text_color="gray"
26 ).pack()
27
28# 机器码展示区
29 machine_frame = ctk.CTkFrame(self, fg_color="transparent")
30 machine_frame.pack(pady=15, padx=40, fill="x")
31
32 self.machine_code = get_machine_code()
33 code_entry = ctk.CTkEntry(
34 machine_frame, width=300,
35 font=ctk.CTkFont(size=14, family="Consolas"),
36 state="readonly"
37 )
38 code_entry.pack(side="left", expand=True, fill="x")
39 code_entry.insert(0, self.machine_code)
40
41 ctk.CTkButton(
42 machine_frame, text="复制", width=60,
43 command=lambda: self._copy_to_clipboard(self.machine_code)
44 ).pack(side="left", padx=(8, 0))
45
46# 激活码输入区
47 ctk.CTkLabel(self, text="激活码:").pack(anchor="w", padx=40)
48 self.key_entry = ctk.CTkEntry(
49 self, width=400,
50 font=ctk.CTkFont(size=13, family="Consolas"),
51 placeholder_text="XXXX-XXXX-XXXX-XXXX-XXXX-XXXX"
52 )
53 self.key_entry.pack(padx=40, pady=(4, 0))
54
55# 状态提示
56 self.status_label = ctk.CTkLabel(
57 self, text="", font=ctk.CTkFont(size=12)
58 )
59 self.status_label.pack(pady=8)
60
61# 激活按钮
62 ctk.CTkButton(
63 self, text="立即激活", width=200,
64 command=self._do_activate
65 ).pack(pady=(0, 20))
66
67def _copy_to_clipboard(self, text: str):
68 self.clipboard_clear()
69 self.clipboard_append(text)
70 self.status_label.configure(text="机器码已复制", text_color="#4CAF50")
71
72def _do_activate(self):
73 key = self.key_entry.get().strip()
74if not key:
75 self.status_label.configure(text="请输入激活码", text_color="#FF5722")
76return
77
78if verify_license(key):
79save_license(key)
80 self.status_label.configure(
81 text="激活成功!程序将在3秒后重启", text_color="#4CAF50"
82 )
83 self.after(3000, self._restart_app)
84else:
85 self.status_label.configure(
86 text="激活码无效,请检查后重试", text_color="#FF5722"
87 )
88
89def _restart_app(self):
90 self.destroy()
91# 实际项目里这里重新启动主窗口
92import main
93 main.launch()
94
95
96if __name__ == "__main__":
97 app = ActivationWindow()
98 app.mainloop()界面长这样:上半部分展示机器码(带一键复制),下半部分让用户输入激活码,点击按钮验证。简洁,够用。
🚀 主程序集成
把授权检查嵌入主程序入口,是整个流程的最后一步。
python1# main.py
2import customtkinter as ctk
3from license_core import is_activated
4
5ctk.set_appearance_mode("dark")
6ctk.set_default_color_theme("blue")
7
8
9class MainApp(ctk.CTk):
10def __init__(self):
11super().__init__()
12 self.title("我的软件 - 已激活")
13 self.geometry("800x500")
14 ctk.CTkLabel(
15 self, text="主功能界面",
16 font=ctk.CTkFont(size=28, weight="bold")
17 ).pack(expand=True)
18
19
20def launch():
21if not is_activated():
22# 未激活,跳转激活窗口
23from ui_activate import ActivationWindow
24 app = ActivationWindow()
25 app.mainloop()
26else:
27 app = MainApp()
28 app.mainloop()
29
30
31if __name__ == "__main__":
32launch()

逻辑很直白:启动时先检查激活状态,没激活就弹激活窗口,激活了才进主界面。
🔑 Key保存到本地
python1def save_license(license_key: str) -> None:
2"""
3 激活成功后将激活码持久化到本地文件
4 下次启动直接读取,无需重新输入
5 """ LICENSE_FILE.write_text(license_key.strip(), encoding="utf-8")
6
7
8def load_license() -> str:
9"""读取已保存的激活码,文件不存在则返回空字符串"""
10if LICENSE_FILE.exists():
11return LICENSE_FILE.read_text(encoding="utf-8").strip()
12return ""
13
14
15def is_activated() -> bool:
16"""
17 启动时调用此函数:
18 1. 读取本地授权文件
19 2. 用当前机器码重新验证
20 3. 两者匹配则直接放行,无需用户操作
21 """ saved_key = load_license()
22if not saved_key:
23return False
24return verify_license(saved_key)⚠️ 几个必须注意的坑
密钥泄露是最大风险。SECRET_KEY 一旦被逆向工程提取,整套系统就形同虚设。建议用 PyInstaller 打包时配合 --key 参数做字节码加密,同时对关键字符串做简单混淆处理,提高提取难度。
虚拟机环境的硬件特征可能不稳定。 有些虚拟机的CPU ID和主板序列号每次启动都不一样,或者返回全零。可以在 get_machine_code() 里加一个兜底策略,当硬件信息明显异常时,混入网卡MAC地址作为补充维度。
授权文件被手动删除后需要重新激活。 这是正常现象,但如果用户频繁重装系统,体验会很差。可以考虑把授权文件存到注册表(Windows),或者用 winreg 模块写入系统级位置,增加被意外删除的难度。
不要在界面上暴露太多调试信息。 错误提示只说"激活码无效",不要说"机器码不匹配"或者"HMAC验证失败"——这些细节会给逆向分析者提供线索。
💬 写在最后
这套方案的强度,定位很清楚:挡住普通用户,不挡专业逆向。如果你的软件面对的是企业客户或者有一定付费意愿的专业用户,这个级别的保护已经够用了。
真正想深入这个方向的话,下一步可以研究:联网验证 + 本地缓存的混合模式、代码混淆工具(如 pyarmor)的配合使用,以及把激活逻辑放进C扩展模块来增加分析难度。
欢迎在评论区聊聊你在软件分发中遇到的实际问题,或者分享你用过的其他授权方案。
夜雨聆风