398 lines
13 KiB
Python
398 lines
13 KiB
Python
# --- START OF FILE auth_validator.py ---
|
||
"""
|
||
Python软件授权验证器 (现代化UI版)
|
||
功能:
|
||
1. 支持在线/离线验证
|
||
2. 自动保存/读取历史卡密
|
||
3. 现代化深色主题 UI
|
||
4. 机器码一键复制
|
||
|
||
使用方法 (完全兼容旧版):
|
||
from auth_validator import AuthValidator
|
||
|
||
validator = AuthValidator(software_id="your_software_id")
|
||
if not validator.validate():
|
||
exit()
|
||
"""
|
||
|
||
import os
|
||
import json
|
||
import time
|
||
import hashlib
|
||
import threading
|
||
import requests
|
||
import platform
|
||
import uuid
|
||
from datetime import datetime, timedelta
|
||
from typing import Optional, Tuple, Dict, Any
|
||
|
||
# 尝试导入现代化UI库,如果未安装则提示
|
||
try:
|
||
import customtkinter as ctk
|
||
import tkinter as tk
|
||
from tkinter import messagebox
|
||
except ImportError:
|
||
print("错误: 请先安装UI库 -> pip install customtkinter")
|
||
exit(1)
|
||
|
||
|
||
# ==========================================
|
||
# 1. 基础设施层 (配置与机器码)
|
||
# ==========================================
|
||
|
||
class ConfigManager:
|
||
"""管理本地配置(如上次使用的卡密)"""
|
||
CONFIG_FILE = "auth_config.json"
|
||
|
||
@classmethod
|
||
def load_config(cls) -> dict:
|
||
if os.path.exists(cls.CONFIG_FILE):
|
||
try:
|
||
with open(cls.CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||
return json.load(f)
|
||
except:
|
||
pass
|
||
return {}
|
||
|
||
@classmethod
|
||
def save_last_key(cls, license_key: str):
|
||
"""保存最后一次成功的卡密"""
|
||
config = cls.load_config()
|
||
config['last_license_key'] = license_key
|
||
try:
|
||
with open(cls.CONFIG_FILE, 'w', encoding='utf-8') as f:
|
||
json.dump(config, f, indent=2)
|
||
except:
|
||
pass
|
||
|
||
@classmethod
|
||
def get_last_key(cls) -> str:
|
||
return cls.load_config().get('last_license_key', '')
|
||
|
||
|
||
class MachineCodeGenerator:
|
||
"""生成稳定的机器码"""
|
||
|
||
@staticmethod
|
||
def get() -> str:
|
||
cache_file = ".machine_id"
|
||
# 优先读取缓存保持不变
|
||
if os.path.exists(cache_file):
|
||
with open(cache_file, 'r') as f:
|
||
return f.read().strip()
|
||
|
||
hw_info = []
|
||
try:
|
||
# 1. 平台信息
|
||
hw_info.append(platform.node())
|
||
hw_info.append(platform.machine())
|
||
|
||
# 2. MAC地址
|
||
hw_info.append(str(uuid.getnode()))
|
||
|
||
# 3. Windows UUID (如果是Windows)
|
||
if platform.system() == "Windows":
|
||
try:
|
||
import subprocess
|
||
cmd = "wmic csproduct get uuid"
|
||
uuid_str = subprocess.check_output(cmd).decode().split('\n')[1].strip()
|
||
hw_info.append(uuid_str)
|
||
except:
|
||
pass
|
||
except:
|
||
hw_info.append(str(uuid.uuid4()))
|
||
|
||
# 生成Hash
|
||
combined = "|".join(hw_info)
|
||
code = hashlib.sha256(combined.encode()).hexdigest()[:32].upper()
|
||
|
||
# 写入缓存
|
||
try:
|
||
with open(cache_file, 'w') as f:
|
||
f.write(code)
|
||
except:
|
||
pass
|
||
|
||
return code
|
||
|
||
|
||
# ==========================================
|
||
# 2. 核心逻辑层 (验证与通信)
|
||
# ==========================================
|
||
|
||
class AuthCore:
|
||
"""处理验证逻辑,不包含UI"""
|
||
|
||
def __init__(self, software_id, api_url, secret_key, timeout=5):
|
||
self.software_id = software_id
|
||
self.api_url = api_url.rstrip('/')
|
||
self.secret_key = secret_key
|
||
self.timeout = timeout
|
||
self.machine_code = MachineCodeGenerator.get()
|
||
self.token_file = f".auth_{software_id}.token"
|
||
|
||
def check_local_cache(self) -> bool:
|
||
"""检查本地缓存是否有效(静默验证)"""
|
||
if not os.path.exists(self.token_file):
|
||
return False
|
||
try:
|
||
with open(self.token_file, 'r') as f:
|
||
data = json.load(f)
|
||
|
||
# 校验机器码
|
||
if data.get('machine_code') != self.machine_code:
|
||
return False
|
||
|
||
# 校验过期时间
|
||
expire_str = data.get('expire_time')
|
||
if expire_str and expire_str != "永久":
|
||
expire_time = datetime.fromisoformat(expire_str)
|
||
if datetime.utcnow() > expire_time:
|
||
return False
|
||
|
||
# 校验本地缓存时效 (例如每7天必须联网一次)
|
||
last_check = datetime.fromisoformat(data.get('last_check', '2000-01-01'))
|
||
if datetime.utcnow() - last_check > timedelta(days=7):
|
||
return False
|
||
|
||
return True
|
||
except:
|
||
return False
|
||
|
||
def verify_online(self, license_key: str) -> Tuple[bool, str, dict]:
|
||
"""在线验证"""
|
||
try:
|
||
# 这里模拟网络请求,请替换为真实的 request.post
|
||
# 真实代码示例:
|
||
"""
|
||
timestamp = int(time.time())
|
||
sign = hashlib.sha256(f"{self.software_id}{license_key}{self.machine_code}{timestamp}{self.secret_key}".encode()).hexdigest()
|
||
resp = requests.post(f"{self.api_url}/verify", json={...}, timeout=self.timeout)
|
||
result = resp.json()
|
||
"""
|
||
|
||
# === 模拟后端返回 (仅供测试,请根据实际API修改) ===
|
||
import time
|
||
time.sleep(0.8) # 模拟网络延迟
|
||
|
||
# 模拟: 只要输入不为空且不含 'FAIL' 就算成功
|
||
if not license_key or "FAIL" in license_key.upper():
|
||
return False, "无效的卡密或订阅已过期", {}
|
||
|
||
fake_response = {
|
||
"success": True,
|
||
"msg": "验证成功",
|
||
"data": {
|
||
"expire_time": (datetime.utcnow() + timedelta(days=30)).isoformat(),
|
||
"machine_code": self.machine_code,
|
||
"last_check": datetime.utcnow().isoformat()
|
||
}
|
||
}
|
||
return True, "验证成功", fake_response["data"]
|
||
# ============================================
|
||
|
||
except Exception as e:
|
||
return False, f"网络连接失败: {str(e)}", {}
|
||
|
||
def save_token(self, data: dict):
|
||
"""验证成功后保存Token"""
|
||
try:
|
||
with open(self.token_file, 'w') as f:
|
||
json.dump(data, f)
|
||
except:
|
||
pass
|
||
|
||
|
||
# ==========================================
|
||
# 3. 现代化 UI 层 (View)
|
||
# ==========================================
|
||
|
||
class AuthWindow(ctk.CTk):
|
||
"""现代化验证窗口"""
|
||
|
||
def __init__(self, auth_core: AuthCore):
|
||
super().__init__()
|
||
self.auth_core = auth_core
|
||
self.is_verified = False # 验证结果状态
|
||
|
||
# 窗口基础设置
|
||
self.title("软件授权验证")
|
||
self.geometry("420x550")
|
||
self.resizable(False, False)
|
||
ctk.set_appearance_mode("Dark")
|
||
ctk.set_default_color_theme("blue")
|
||
|
||
# 居中显示
|
||
self._center_window()
|
||
self._setup_ui()
|
||
|
||
# 自动填入上次卡密
|
||
self._load_history()
|
||
|
||
def _center_window(self):
|
||
self.update_idletasks()
|
||
width = self.winfo_width()
|
||
height = self.winfo_height()
|
||
x = (self.winfo_screenwidth() // 2) - (width // 2)
|
||
y = (self.winfo_screenheight() // 2) - (height // 2)
|
||
self.geometry(f'{width}x{height}+{x}+{y}')
|
||
|
||
def _setup_ui(self):
|
||
# 1. 头部图标与标题
|
||
self.header = ctk.CTkFrame(self, fg_color="transparent")
|
||
self.header.pack(pady=(40, 20))
|
||
|
||
ctk.CTkLabel(self.header, text="🔐", font=("Segoe UI Emoji", 56)).pack()
|
||
ctk.CTkLabel(self.header, text="用户授权系统", font=("Microsoft YaHei UI", 22, "bold")).pack(pady=5)
|
||
|
||
# 2. 机器码显示区
|
||
self.mc_frame = ctk.CTkFrame(self, fg_color="#2B2B2B", corner_radius=8)
|
||
self.mc_frame.pack(padx=30, pady=10, fill="x")
|
||
|
||
ctk.CTkLabel(self.mc_frame, text="机器码 (点击复制):", font=("Microsoft YaHei UI", 12), text_color="gray").pack(
|
||
anchor="w", padx=15, pady=(10, 0))
|
||
|
||
self.mc_btn = ctk.CTkButton(
|
||
self.mc_frame,
|
||
text=self.auth_core.machine_code,
|
||
font=("Consolas", 11),
|
||
fg_color="transparent",
|
||
hover_color="#333333",
|
||
anchor="w",
|
||
command=self._copy_machine_code
|
||
)
|
||
self.mc_btn.pack(fill="x", padx=5, pady=(0, 10))
|
||
|
||
# 3. 卡密输入区
|
||
self.input_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||
self.input_frame.pack(padx=30, pady=10, fill="x")
|
||
|
||
ctk.CTkLabel(self.input_frame, text="请输入激活码 / 卡密:", font=("Microsoft YaHei UI", 14)).pack(anchor="w",
|
||
pady=(0, 5))
|
||
|
||
self.entry_key = ctk.CTkEntry(
|
||
self.input_frame,
|
||
height=45,
|
||
placeholder_text="XXXXX-XXXXX-XXXXX-XXXXX",
|
||
font=("Consolas", 14),
|
||
border_width=2
|
||
)
|
||
self.entry_key.pack(fill="x")
|
||
|
||
# 4. 状态提示信息
|
||
self.lbl_status = ctk.CTkLabel(self, text="等待验证...", text_color="gray", font=("Microsoft YaHei UI", 12))
|
||
self.lbl_status.pack(pady=(20, 5))
|
||
|
||
# 5. 验证按钮
|
||
self.btn_verify = ctk.CTkButton(
|
||
self,
|
||
text="立即验证授权",
|
||
height=50,
|
||
font=("Microsoft YaHei UI", 16, "bold"),
|
||
command=self._handle_verify
|
||
)
|
||
self.btn_verify.pack(padx=30, pady=20, fill="x")
|
||
|
||
# 底部版权
|
||
ctk.CTkLabel(self, text="Powered by AuthValidator", font=("Arial", 10), text_color="#444").pack(side="bottom",
|
||
pady=10)
|
||
|
||
def _load_history(self):
|
||
"""读取历史卡密"""
|
||
last_key = ConfigManager.get_last_key()
|
||
if last_key:
|
||
self.entry_key.insert(0, last_key)
|
||
self.lbl_status.configure(text="已自动填入上次卡密,请点击验证", text_color="#888")
|
||
|
||
def _copy_machine_code(self):
|
||
self.clipboard_clear()
|
||
self.clipboard_append(self.auth_core.machine_code)
|
||
self.lbl_status.configure(text="✅ 机器码已复制到剪贴板", text_color="#4CAF50")
|
||
self.after(2000, lambda: self.lbl_status.configure(text="等待验证...", text_color="gray"))
|
||
|
||
def _handle_verify(self):
|
||
key = self.entry_key.get().strip()
|
||
if not key:
|
||
self.lbl_status.configure(text="❌ 卡密不能为空", text_color="#F44336")
|
||
return
|
||
|
||
# 锁定UI
|
||
self.btn_verify.configure(state="disabled", text="正在连接服务器...")
|
||
self.entry_key.configure(state="disabled")
|
||
self.lbl_status.configure(text="⏳ 正在验证中,请稍候...", text_color="#2196F3")
|
||
|
||
# 开启线程进行验证
|
||
threading.Thread(target=self._verify_thread, args=(key,), daemon=True).start()
|
||
|
||
def _verify_thread(self, key):
|
||
"""后台验证逻辑"""
|
||
success, msg, data = self.auth_core.verify_online(key)
|
||
# 回到主线程更新UI
|
||
self.after(0, lambda: self._on_verify_result(success, msg, key, data))
|
||
|
||
def _on_verify_result(self, success, msg, key, data):
|
||
self.btn_verify.configure(state="normal", text="立即验证授权")
|
||
self.entry_key.configure(state="normal")
|
||
|
||
if success:
|
||
# 验证成功
|
||
self.lbl_status.configure(text=f"✅ {msg}", text_color="#4CAF50")
|
||
self.is_verified = True
|
||
|
||
# 保存卡密和Token
|
||
ConfigManager.save_last_key(key)
|
||
self.auth_core.save_token(data)
|
||
|
||
# 延迟关闭窗口
|
||
self.after(1000, self.destroy)
|
||
else:
|
||
# 验证失败
|
||
self.lbl_status.configure(text=f"❌ {msg}", text_color="#F44336")
|
||
|
||
|
||
# ==========================================
|
||
# 4. 统一接口层 (Facade)
|
||
# ==========================================
|
||
|
||
class AuthValidator:
|
||
"""
|
||
授权验证器主类
|
||
保持与旧版一致的调用接口
|
||
"""
|
||
|
||
def __init__(self,
|
||
software_id: str,
|
||
api_url: str = "http://localhost:5000/api/v1",
|
||
secret_key: str = "default_secret",
|
||
**kwargs):
|
||
"""
|
||
初始化验证器
|
||
Args:
|
||
software_id: 软件ID
|
||
api_url: API地址
|
||
secret_key: 密钥
|
||
"""
|
||
self.core = AuthCore(software_id, api_url, secret_key)
|
||
|
||
def validate(self) -> bool:
|
||
"""
|
||
执行验证流程 (阻塞式)
|
||
1. 优先尝试静默验证(本地缓存)
|
||
2. 失败则弹出现代化UI窗口
|
||
Returns:
|
||
bool: 是否验证成功
|
||
"""
|
||
# 1. 尝试静默验证 (本地Token有效)
|
||
if self.core.check_local_cache():
|
||
return True
|
||
|
||
# 2. 启动 UI 窗口
|
||
app = AuthWindow(self.core)
|
||
|
||
# 运行主循环 (这会阻塞代码执行,直到窗口关闭)
|
||
app.mainloop()
|
||
|
||
# 3. 窗口关闭后,检查是否验证成功
|
||
return app.is_verified
|
||
|
||
# --- END OF FILE --- |