做过桌面工具的开发者,大概都遇到过这种情况——花了几个月心血写出来的小工具,发出去没多久就被人打包转发,甚至有人直接拿去卖钱。气不气?当然气。但更现实的问题是:怎么防?
完全防住是不可能的,这一点咱们得想开。逆向工程、内存dump、代码混淆绕过……高手面前没有铜墙铁壁。但我们的目标不是对抗顶级黑客,而是提高普通用户随意传播的门槛,让"复制粘贴就能用"这条路走不通。
本文要做的事很具体:用 CustomTkinter 搭一套基于机器码绑定的本地授权系统,包含激活码生成、验证、界面集成的完整流程。代码全部可以直接跑,没有废话。
很多人一上来就问"用什么加密算法",其实这个问题排在第二位。第一位的问题是:你的授权凭证跟什么绑定?
常见的绑定维度有三种:
账号绑定——需要联网验证,服务器说你有权限你才有。灵活,但需要维护后端,断网就凉。
设备绑定——把激活码跟某台机器的硬件特征挂钩,换台电脑就失效。离线可用,实现相对简单。
时间绑定——设置有效期,到期自动失效。通常和前两种结合使用。
咱们今天做的是设备绑定 + 本地验证的方案。核心逻辑是这样的:
用户机器 → 采集硬件特征 → 生成机器码 开发者拿到机器码 → 用密钥生成激活码 用户输入激活码 → 本地验证 → 解锁功能
没有服务器,没有联网请求,所有验证在本地完成。简单、稳定,适合个人开发者和小团队。
依赖不多,几行装完:
bashpip 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,把机器码作为消息,密钥是我们自己保管的那串字符。
pythondef 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 里,只在你自己的机器上运行,绝对不要打包进发布版本。
用户输入激活码后,程序重新计算一遍"如果这台机器的机器码对应的激活码应该是什么",然后和用户输入的做比对。匹配就通过,不匹配就拒绝。
pythondef 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 这个细节——它保证比较时间恒定,防止通过响应时间差来猜测激活码。这种攻击在本地场景下威胁不大,但养成好习惯没坏处。
核心逻辑有了,现在给它穿件衣服。
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()


逻辑很直白:启动时先检查激活状态,没激活就弹激活窗口,激活了才进主界面。
pythondef 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 许可协议。转载请注明出处!