Kamixitong/app/utils/auth_validator.py

761 lines
24 KiB
Python
Raw Normal View History

2025-11-11 21:39:12 +08:00
"""
2025-11-17 12:30:12 +08:00
Python软件授权验证器 - 现代化GUI版本
集成了CustomTkinter实现的Material Design风格界面
2025-11-11 21:39:12 +08:00
"""
import os
import json
import time
import hashlib
import requests
from datetime import datetime, timedelta
from typing import Optional, Tuple, Dict, Any
2025-11-17 12:30:12 +08:00
import threading
# 尝试导入CustomTkinter如果失败则使用标准tkinter
try:
import customtkinter as ctk
CTK_AVAILABLE = True
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue")
# 设置主题和颜色
# ctk.set_appearance_mode("dark") # 可选: "light", "dark", "system"
# ctk.set_default_color_theme("blue") # 可选: "blue", "green", "dark-blue"
except ImportError:
CTK_AVAILABLE = False
import tkinter as tk
from tkinter import simpledialog, messagebox
# ===== 原有的类保持不变 =====
2025-11-11 21:39:12 +08:00
class MachineCodeGenerator:
"""独立版本的机器码生成器"""
@staticmethod
def generate() -> str:
"""生成32位机器码"""
import platform
import uuid
hw_info = []
try:
system = platform.system().lower()
try:
system_uuid = str(uuid.getnode())
if system_uuid and system_uuid != '0':
hw_info.append(system_uuid)
except:
pass
try:
hostname = platform.node()
if hostname:
hw_info.append(hostname)
except:
pass
try:
system_info = f"{system}_{platform.release()}_{platform.machine()}"
hw_info.append(system_info)
except:
pass
try:
python_version = platform.python_version()
hw_info.append(python_version)
except:
pass
except Exception as e:
print(f"获取硬件信息时出错: {e}")
if not hw_info:
hw_info = [str(uuid.uuid4()), str(uuid.uuid4())]
combined_info = '|'.join(hw_info)
hash_obj = hashlib.sha256(combined_info.encode('utf-8'))
machine_code = hash_obj.hexdigest()[:32].upper()
return machine_code
2025-11-17 12:30:12 +08:00
2025-11-11 21:39:12 +08:00
class SimpleCrypto:
"""简单的加密解密工具"""
2025-11-16 13:33:32 +08:00
2025-11-11 21:39:12 +08:00
@staticmethod
def generate_hash(data: str, salt: str = "") -> str:
"""生成哈希值"""
combined = f"{data}{salt}".encode('utf-8')
return hashlib.sha256(combined).hexdigest()
2025-11-16 13:33:32 +08:00
2025-11-11 21:39:12 +08:00
@staticmethod
def generate_signature(data: str, secret_key: str) -> str:
"""生成签名"""
combined = f"{data}{secret_key}".encode('utf-8')
return hashlib.sha256(combined).hexdigest()
2025-11-17 12:30:12 +08:00
2025-11-11 21:39:12 +08:00
class AuthCache:
"""授权信息缓存管理"""
def __init__(self, cache_file: str = ".auth_cache"):
self.cache_file = cache_file
self.cache_data = self._load_cache()
def _load_cache(self) -> Dict[str, Any]:
"""加载缓存"""
try:
if os.path.exists(self.cache_file):
with open(self.cache_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception:
pass
return {}
def _save_cache(self):
"""保存缓存"""
try:
with open(self.cache_file, 'w', encoding='utf-8') as f:
json.dump(self.cache_data, f, ensure_ascii=False, indent=2)
except Exception:
pass
def get_auth_info(self, software_id: str) -> Optional[Dict[str, Any]]:
"""获取授权信息"""
key = f"auth_{software_id}"
return self.cache_data.get(key)
def set_auth_info(self, software_id: str, auth_info: Dict[str, Any]):
"""设置授权信息"""
key = f"auth_{software_id}"
self.cache_data[key] = auth_info
self._save_cache()
def clear_cache(self, software_id = None):
"""清除缓存"""
if software_id:
key = f"auth_{software_id}"
self.cache_data.pop(key, None)
else:
self.cache_data.clear()
self._save_cache()
2025-11-17 12:30:12 +08:00
# ===== 现代化GUI组件 =====
class ModernAuthDialog:
"""现代化授权验证对话框"""
def __init__(self,
software_id: str,
machine_code: str,
on_verify_callback,
parent=None):
"""初始化对话框"""
self.software_id = software_id
self.machine_code = machine_code
self.on_verify_callback = on_verify_callback
self.result = None
self.license_key = None
if CTK_AVAILABLE:
self.window = ctk.CTk()
else:
self.window = tk.Tk()
self._setup_window()
self._create_widgets()
def _setup_window(self):
"""设置窗口属性"""
self.window.title("软件授权验证")
if CTK_AVAILABLE:
self.window.geometry("400x400")
else:
self.window.geometry("350x400")
self.window.resizable(False, False)
# 窗口居中
self.window.update_idletasks()
width = self.window.winfo_width()
height = self.window.winfo_height()
x = (self.window.winfo_screenwidth() // 2) - (width // 2)
y = (self.window.winfo_screenheight() // 2) - (height // 2)
self.window.geometry(f'{width}x{height}+{x}+{y}')
def _create_widgets(self):
"""创建界面组件"""
if CTK_AVAILABLE:
self._create_modern_widgets()
else:
self._create_classic_widgets()
def _create_modern_widgets(self):
"""创建现代化界面CustomTkinter"""
main_frame = ctk.CTkFrame(self.window, fg_color="transparent")
main_frame.pack(fill="both", expand=True, padx=30, pady=30)
# Logo区域
logo_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
logo_frame.pack(fill="x", pady=(0, 20))
ctk.CTkLabel(
logo_frame,
text="🔐",
font=ctk.CTkFont(size=60)
).pack(pady=(0, 10))
ctk.CTkLabel(
logo_frame,
text="软件授权验证",
font=ctk.CTkFont(size=28, weight="bold")
).pack()
ctk.CTkLabel(
logo_frame,
text="请输入您的授权卡密以继续使用",
font=ctk.CTkFont(size=13),
text_color="gray60"
).pack(pady=(5, 0))
# 信息卡片
info_frame = ctk.CTkFrame(main_frame)
info_frame.pack(fill="x", pady=20)
# 软件ID
software_id_frame = ctk.CTkFrame(info_frame, fg_color="transparent")
software_id_frame.pack(fill="x", padx=20, pady=(15, 8))
ctk.CTkLabel(
software_id_frame,
text="软件ID",
font=ctk.CTkFont(size=12, weight="bold"),
text_color="gray70"
).pack(anchor="w")
ctk.CTkLabel(
software_id_frame,
text=self.software_id,
font=ctk.CTkFont(size=14),
text_color="white"
).pack(anchor="w", pady=(5, 0))
ctk.CTkFrame(info_frame, height=1, fg_color="gray30").pack(fill="x", padx=20, pady=8)
# 机器码
machine_code_frame = ctk.CTkFrame(info_frame, fg_color="transparent")
machine_code_frame.pack(fill="x", padx=20, pady=(8, 15))
ctk.CTkLabel(
machine_code_frame,
text="机器码",
font=ctk.CTkFont(size=12, weight="bold"),
text_color="gray70"
).pack(anchor="w")
machine_code_display = f"{self.machine_code[:8]}...{self.machine_code[-8:]}"
ctk.CTkLabel(
machine_code_frame,
text=machine_code_display,
font=ctk.CTkFont(size=14, family="Courier"),
text_color="white"
).pack(anchor="w", pady=(5, 0))
# 输入区域
input_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
input_frame.pack(fill="x", pady=20)
ctk.CTkLabel(
input_frame,
text="授权卡密",
font=ctk.CTkFont(size=14, weight="bold"),
anchor="w"
).pack(fill="x", pady=(0, 10))
self.license_entry = ctk.CTkEntry(
input_frame,
height=45,
font=ctk.CTkFont(size=14),
placeholder_text="请输入您的授权卡密",
border_width=2
)
self.license_entry.pack(fill="x")
self.license_entry.bind("<Return>", lambda e: self._verify_license())
ctk.CTkLabel(
input_frame,
text="💡 试用卡密以 'TRIAL_' 开头",
font=ctk.CTkFont(size=11),
text_color="gray60"
).pack(anchor="w", pady=(8, 0))
# 状态区域
self.status_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
self.status_frame.pack(fill="x", pady=(10, 20))
self.status_label = ctk.CTkLabel(
self.status_frame,
text="",
font=ctk.CTkFont(size=12),
text_color="gray60",
wraplength=400
)
self.status_label.pack()
# 按钮区域
button_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
button_frame.pack(fill="x", pady=(10, 0))
self.verify_button = ctk.CTkButton(
button_frame,
text="验证授权",
height=45,
font=ctk.CTkFont(size=15, weight="bold"),
command=self._verify_license,
corner_radius=10
)
self.verify_button.pack(fill="x", pady=(0, 10))
self.cancel_button = ctk.CTkButton(
button_frame,
text="取消",
height=40,
font=ctk.CTkFont(size=14),
command=self._cancel,
fg_color="gray30",
hover_color="gray40",
corner_radius=10
)
self.cancel_button.pack(fill="x")
self.license_entry.focus()
def _create_classic_widgets(self):
"""创建经典界面标准Tkinter"""
main_frame = tk.Frame(self.window, bg="white")
main_frame.pack(fill="both", expand=True, padx=20, pady=20)
# 标题
tk.Label(
main_frame,
text="🔐 软件授权验证",
font=("Arial", 20, "bold"),
bg="white"
).pack(pady=(10, 20))
# 信息框
info_frame = tk.LabelFrame(main_frame, text="授权信息", font=("Arial", 10), bg="white")
info_frame.pack(fill="x", pady=10)
tk.Label(info_frame, text=f"软件ID: {self.software_id}", bg="white").pack(anchor="w", padx=10, pady=5)
tk.Label(info_frame, text=f"机器码: {self.machine_code[:16]}...", bg="white").pack(anchor="w", padx=10, pady=5)
# 输入框
input_frame = tk.Frame(main_frame, bg="white")
input_frame.pack(fill="x", pady=20)
tk.Label(input_frame, text="请输入授权卡密:", font=("Arial", 11), bg="white").pack(anchor="w")
self.license_entry = tk.Entry(input_frame, font=("Arial", 12), width=40)
self.license_entry.pack(fill="x", pady=10)
self.license_entry.bind("<Return>", lambda e: self._verify_license())
# 状态标签
self.status_label = tk.Label(main_frame, text="", font=("Arial", 10), bg="white", fg="red")
self.status_label.pack(pady=10)
# 按钮
button_frame = tk.Frame(main_frame, bg="white")
button_frame.pack(fill="x", pady=10)
self.verify_button = tk.Button(
button_frame,
text="验证授权",
font=("Arial", 11, "bold"),
bg="#4CAF50",
fg="white",
command=self._verify_license,
width=15,
height=2
)
self.verify_button.pack(side="left", padx=5)
self.cancel_button = tk.Button(
button_frame,
text="取消",
font=("Arial", 11),
command=self._cancel,
width=15,
height=2
)
self.cancel_button.pack(side="left", padx=5)
self.license_entry.focus()
def _show_status(self, message: str, is_error: bool = False):
"""显示状态消息"""
if CTK_AVAILABLE:
color = "#ff5555" if is_error else "#50fa7b"
icon = "" if is_error else ""
self.status_label.configure(text=f"{icon} {message}", text_color=color)
else:
color = "red" if is_error else "green"
self.status_label.configure(text=message, fg=color)
def _verify_license(self):
"""验证卡密"""
license_key = self.license_entry.get().strip()
if not license_key:
self._show_status("请输入授权卡密", is_error=True)
return
self.license_key = license_key
# 禁用控件
if CTK_AVAILABLE:
self.verify_button.configure(state="disabled", text="验证中...")
self.cancel_button.configure(state="disabled")
self.license_entry.configure(state="disabled")
else:
self.verify_button.configure(state="disabled", text="验证中...")
self.cancel_button.configure(state="disabled")
self.license_entry.configure(state="disabled")
# 在后台线程执行验证
def verify_thread():
success, message, auth_info = self.on_verify_callback(license_key)
self.window.after(0, lambda: self._on_verify_complete(success, message, auth_info))
thread = threading.Thread(target=verify_thread, daemon=True)
thread.start()
def _on_verify_complete(self, success: bool, message: str, auth_info: Any):
"""验证完成"""
# 恢复控件
if CTK_AVAILABLE:
self.verify_button.configure(state="normal", text="验证授权")
self.cancel_button.configure(state="normal")
self.license_entry.configure(state="normal")
else:
self.verify_button.configure(state="normal", text="验证授权")
self.cancel_button.configure(state="normal")
self.license_entry.configure(state="normal")
if success:
self._show_status("验证成功!", is_error=False)
self.result = (True, message, auth_info)
self.window.after(1000, self.window.destroy)
else:
self._show_status(message, is_error=True)
self.license_entry.delete(0, "end" if not CTK_AVAILABLE else "end")
self.license_entry.focus()
def _cancel(self):
"""取消"""
self.result = (False, "用户取消", None)
self.window.destroy()
def show(self):
"""显示对话框"""
self.window.mainloop()
return self.result if self.result else (False, "用户取消", None)
class ModernMessageBox:
"""现代化消息框"""
@staticmethod
def show_info(title: str, message: str):
"""显示信息"""
if CTK_AVAILABLE:
ModernMessageBox._show_ctk_message(title, message, is_error=False)
else:
messagebox.showinfo(title, message)
@staticmethod
def show_error(title: str, message: str):
"""显示错误"""
if CTK_AVAILABLE:
ModernMessageBox._show_ctk_message(title, message, is_error=True)
else:
messagebox.showerror(title, message)
@staticmethod
def _show_ctk_message(title: str, message: str, is_error: bool):
"""显示CTK消息框"""
dialog = ctk.CTk()
dialog.title(title)
dialog.geometry("400x250")
dialog.resizable(False, False)
# 居中
dialog.update_idletasks()
x = (dialog.winfo_screenwidth() // 2) - 200
y = (dialog.winfo_screenheight() // 2) - 125
dialog.geometry(f'400x250+{x}+{y}')
main_frame = ctk.CTkFrame(dialog, fg_color="transparent")
main_frame.pack(fill="both", expand=True, padx=30, pady=30)
# 图标
icon = "" if is_error else ""
color = "#ff5555" if is_error else "#50fa7b"
ctk.CTkLabel(
main_frame,
text=icon,
font=ctk.CTkFont(size=50),
text_color=color
).pack(pady=(0, 15))
ctk.CTkLabel(
main_frame,
text=title,
font=ctk.CTkFont(size=20, weight="bold")
).pack(pady=(0, 10))
ctk.CTkLabel(
main_frame,
text=message,
font=ctk.CTkFont(size=13),
text_color="gray70",
wraplength=340
).pack(pady=(0, 20))
ctk.CTkButton(
main_frame,
text="确定",
height=40,
font=ctk.CTkFont(size=14),
command=dialog.destroy,
corner_radius=10,
fg_color="gray30" if is_error else None,
hover_color="gray40" if is_error else None
).pack(fill="x")
dialog.mainloop()
# ===== AuthValidator类集成GUI=====
2025-11-11 21:39:12 +08:00
class AuthValidator:
"""授权验证器主类"""
def __init__(self,
software_id: str,
api_url: str = "http://localhost:5000/api/v1",
2025-11-16 13:33:32 +08:00
secret_key: str = "taiyi1224",
2025-11-11 21:39:12 +08:00
cache_days: int = 7,
timeout: int = 3,
2025-11-17 12:30:12 +08:00
gui_mode: bool = True): # 默认启用GUI
"""初始化验证器"""
2025-11-11 21:39:12 +08:00
self.software_id = software_id
self.api_url = api_url.rstrip('/')
self.secret_key = secret_key
self.cache_days = cache_days
self.timeout = timeout
self.gui_mode = gui_mode
self.machine_generator = MachineCodeGenerator()
self.cache = AuthCache()
self.machine_code = self._get_or_generate_machine_code()
self.failed_attempts = 0
self.last_attempt_time = None
self.locked_until = None
def _get_or_generate_machine_code(self) -> str:
"""获取或生成机器码"""
cache_file = ".machine_code"
try:
if os.path.exists(cache_file):
with open(cache_file, 'r') as f:
cached_code = f.read().strip()
if len(cached_code) == 32:
return cached_code
except Exception:
pass
machine_code = self.machine_generator.generate()
try:
with open(cache_file, 'w') as f:
f.write(machine_code)
except Exception:
pass
return machine_code
def _validate_license_format(self, license_key: str) -> bool:
2025-11-16 19:06:49 +08:00
"""验证卡密格式"""
2025-11-11 21:39:12 +08:00
if not license_key:
return False
license_key = license_key.strip().replace(' ', '').replace('\t', '').upper()
2025-11-16 19:06:49 +08:00
if len(license_key) < 16 or len(license_key) > 32:
2025-11-11 21:39:12 +08:00
return False
2025-11-16 19:06:49 +08:00
import re
pattern = r'^[A-Z0-9_]+$'
return bool(re.match(pattern, license_key))
2025-11-11 21:39:12 +08:00
def _online_verify(self, license_key: str) -> Tuple[bool, str, Optional[Dict[str, Any]]]:
"""在线验证"""
try:
url = f"{self.api_url}/auth/verify"
timestamp = int(datetime.utcnow().timestamp())
data = {
"software_id": self.software_id,
"license_key": license_key,
"machine_code": self.machine_code,
"timestamp": timestamp
}
signature_data = f"{data['software_id']}{data['license_key']}{data['machine_code']}{data['timestamp']}"
signature = SimpleCrypto.generate_signature(signature_data, self.secret_key)
data["signature"] = signature
response = requests.post(
url,
json=data,
timeout=self.timeout,
headers={'Content-Type': 'application/json'}
)
if response.status_code == 200:
result = response.json()
if result.get('success'):
auth_info = result.get('data', {})
return True, "验证成功", auth_info
else:
return False, result.get('message', '验证失败'), None
else:
return False, f"网络请求失败: {response.status_code}", None
except requests.exceptions.Timeout:
return False, "网络超时,请检查网络连接", None
except requests.exceptions.ConnectionError:
return False, "无法连接到服务器", None
except Exception as e:
return False, f"验证过程出错: {str(e)}", None
def _offline_verify(self) -> Tuple[bool, str]:
"""离线验证"""
auth_info = self.cache.get_auth_info(self.software_id)
if not auth_info:
return False, "未找到有效的离线授权信息,请联网验证"
cache_time = datetime.fromisoformat(auth_info.get('cache_time', ''))
if datetime.utcnow() - cache_time > timedelta(days=self.cache_days):
return False, f"离线授权已过期(超过{self.cache_days}天),请联网验证"
if auth_info.get('machine_code') != self.machine_code:
return False, "设备信息不匹配,请重新验证"
expire_time = auth_info.get('expire_time')
if expire_time:
expire_datetime = datetime.fromisoformat(expire_time)
if datetime.utcnow() > expire_datetime:
return False, "授权已过期,请重新验证"
return True, "离线验证成功"
def _cache_auth_info(self, auth_info: Dict[str, Any]):
"""缓存授权信息"""
auth_info['cache_time'] = datetime.utcnow().isoformat()
self.cache.set_auth_info(self.software_id, auth_info)
def validate(self) -> bool:
2025-11-17 12:30:12 +08:00
"""执行验证流程"""
2025-11-11 21:39:12 +08:00
# 首先尝试离线验证
offline_success, offline_message = self._offline_verify()
if offline_success:
return True
2025-11-17 12:30:12 +08:00
# 离线验证失败显示GUI输入卡密
if self.gui_mode:
dialog = ModernAuthDialog(
software_id=self.software_id,
machine_code=self.machine_code,
on_verify_callback=self._online_verify
)
2025-11-11 21:39:12 +08:00
2025-11-17 12:30:12 +08:00
success, message, auth_info = dialog.show()
2025-11-16 19:06:49 +08:00
if success and auth_info:
self._cache_auth_info(auth_info)
force_update = auth_info.get('force_update', False)
download_url = auth_info.get('download_url')
new_version = auth_info.get('new_version')
if force_update and download_url:
2025-11-17 12:30:12 +08:00
ModernMessageBox.show_error(
"需要更新",
f"发现新版本 {new_version}\n请下载更新后重新启动程序"
)
2025-11-16 19:06:49 +08:00
try:
import webbrowser
webbrowser.open(download_url)
except:
pass
return False
2025-11-17 12:30:12 +08:00
expire_time = auth_info.get('expire_time', '永久')
ModernMessageBox.show_info(
"验证成功",
f"授权验证成功!\n\n有效期至: {expire_time}"
)
2025-11-11 21:39:12 +08:00
return True
else:
2025-11-17 12:30:12 +08:00
return False
else:
# 命令行模式(保留原有逻辑)
print("命令行模式暂未实现请使用gui_mode=True")
return False
2025-11-16 19:06:49 +08:00
def clear_cache(self):
"""清除本地缓存"""
self.cache.clear_cache(self.software_id)
try:
if os.path.exists(".machine_code"):
os.remove(".machine_code")
except Exception:
pass
2025-11-17 12:30:12 +08:00
# ===== 便捷函数 =====
2025-11-16 19:06:49 +08:00
2025-11-17 12:30:12 +08:00
def validate_license(software_id: str, **kwargs) -> bool:
"""便捷的验证函数"""
2025-11-16 19:06:49 +08:00
validator = AuthValidator(software_id, **kwargs)
return validator.validate()
2025-11-11 21:39:12 +08:00
2025-11-17 12:30:12 +08:00
2025-11-16 19:06:49 +08:00
def get_machine_code() -> str:
"""获取当前机器码"""
2025-11-17 12:30:12 +08:00
return MachineCodeGenerator.generate()
# ===== 测试代码 =====
if __name__ == "__main__":
# 测试验证器
validator = AuthValidator(
software_id="TEST_SOFTWARE_001",
api_url="http://localhost:5000/api/v1",
gui_mode=True
)
if validator.validate():
print("✓ 授权验证成功!")
else:
print("✗ 授权验证失败!")