Kamixitong/app/utils/auth_validator.py
2025-11-17 12:30:12 +08:00

761 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Python软件授权验证器 - 现代化GUI版本
集成了CustomTkinter实现的Material Design风格界面
"""
import os
import json
import time
import hashlib
import requests
from datetime import datetime, timedelta
from typing import Optional, Tuple, Dict, Any
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
# ===== 原有的类保持不变 =====
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
class SimpleCrypto:
"""简单的加密解密工具"""
@staticmethod
def generate_hash(data: str, salt: str = "") -> str:
"""生成哈希值"""
combined = f"{data}{salt}".encode('utf-8')
return hashlib.sha256(combined).hexdigest()
@staticmethod
def generate_signature(data: str, secret_key: str) -> str:
"""生成签名"""
combined = f"{data}{secret_key}".encode('utf-8')
return hashlib.sha256(combined).hexdigest()
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()
# ===== 现代化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=====
class AuthValidator:
"""授权验证器主类"""
def __init__(self,
software_id: str,
api_url: str = "http://localhost:5000/api/v1",
secret_key: str = "taiyi1224",
cache_days: int = 7,
timeout: int = 3,
gui_mode: bool = True): # 默认启用GUI
"""初始化验证器"""
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:
"""验证卡密格式"""
if not license_key:
return False
license_key = license_key.strip().replace(' ', '').replace('\t', '').upper()
if len(license_key) < 16 or len(license_key) > 32:
return False
import re
pattern = r'^[A-Z0-9_]+$'
return bool(re.match(pattern, license_key))
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:
"""执行验证流程"""
# 首先尝试离线验证
offline_success, offline_message = self._offline_verify()
if offline_success:
return True
# 离线验证失败显示GUI输入卡密
if self.gui_mode:
dialog = ModernAuthDialog(
software_id=self.software_id,
machine_code=self.machine_code,
on_verify_callback=self._online_verify
)
success, message, auth_info = dialog.show()
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:
ModernMessageBox.show_error(
"需要更新",
f"发现新版本 {new_version}\n请下载更新后重新启动程序"
)
try:
import webbrowser
webbrowser.open(download_url)
except:
pass
return False
expire_time = auth_info.get('expire_time', '永久')
ModernMessageBox.show_info(
"验证成功",
f"授权验证成功!\n\n有效期至: {expire_time}"
)
return True
else:
return False
else:
# 命令行模式(保留原有逻辑)
print("命令行模式暂未实现请使用gui_mode=True")
return False
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
# ===== 便捷函数 =====
def validate_license(software_id: str, **kwargs) -> bool:
"""便捷的验证函数"""
validator = AuthValidator(software_id, **kwargs)
return validator.validate()
def get_machine_code() -> str:
"""获取当前机器码"""
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("✗ 授权验证失败!")