2025-10-23 18:28:10 +08:00
|
|
|
|
"""
|
2025-10-25 17:49:09 +08:00
|
|
|
|
安全的验证器 V2 - 改进版
|
|
|
|
|
|
主要改进:
|
|
|
|
|
|
1. 持久化缓存存储(不会被系统清理)
|
|
|
|
|
|
2. 智能验证策略(已激活用户不频繁打扰)
|
|
|
|
|
|
3. 到期提醒功能
|
|
|
|
|
|
4. 增强的离线验证
|
|
|
|
|
|
5. 改善的用户界面
|
|
|
|
|
|
|
2025-10-23 18:28:10 +08:00
|
|
|
|
作者:太一
|
2025-10-25 17:49:09 +08:00
|
|
|
|
微信:taiyi1224
|
2025-10-23 18:28:10 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import json
|
|
|
|
|
|
import hashlib
|
|
|
|
|
|
import tempfile
|
|
|
|
|
|
import subprocess
|
|
|
|
|
|
import tkinter as tk
|
|
|
|
|
|
from tkinter import messagebox
|
|
|
|
|
|
import threading
|
|
|
|
|
|
import time
|
|
|
|
|
|
import hmac
|
|
|
|
|
|
import base64
|
|
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
from cryptography.fernet import Fernet
|
|
|
|
|
|
import requests
|
2025-10-25 17:49:09 +08:00
|
|
|
|
import urllib3
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
import sqlite3
|
|
|
|
|
|
|
|
|
|
|
|
# 禁用SSL警告(用于自签名证书)
|
|
|
|
|
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
2025-10-23 18:28:10 +08:00
|
|
|
|
|
|
|
|
|
|
# ========== 配置区(这些可以公开) ==========
|
2025-10-25 17:49:09 +08:00
|
|
|
|
API_BASE_URL = "http://localhost:5100/" # 使用本地API服务器
|
|
|
|
|
|
API_KEY = "taiyi1224taiyi1224taiyi1224taiyi1224taiyi1224taiyi1224taiyi1224" # 从环境变量读取更安全
|
2025-10-23 18:28:10 +08:00
|
|
|
|
SOFTWARE_NAME = "SOFTWARE_NAME_PLACEHOLDER" # 打包时替换
|
|
|
|
|
|
|
|
|
|
|
|
# ========== 机器码生成 ==========
|
|
|
|
|
|
def get_machine_code():
|
|
|
|
|
|
"""生成机器码(简化且稳定的版本)"""
|
|
|
|
|
|
import platform
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
if platform.system() == 'Windows':
|
|
|
|
|
|
# 优先使用主板序列号
|
|
|
|
|
|
try:
|
|
|
|
|
|
import subprocess
|
|
|
|
|
|
result = subprocess.check_output(
|
|
|
|
|
|
'wmic baseboard get serialnumber',
|
|
|
|
|
|
shell=True, stderr=subprocess.DEVNULL, timeout=5
|
|
|
|
|
|
).decode().strip()
|
|
|
|
|
|
|
|
|
|
|
|
lines = [line.strip() for line in result.split('\n') if line.strip()]
|
|
|
|
|
|
if len(lines) > 1 and lines[1] not in ['To Be Filled By O.E.M.', '']:
|
|
|
|
|
|
return hashlib.md5(lines[1].encode()).hexdigest()[:16].upper()
|
|
|
|
|
|
except:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# 备用方案:使用MAC地址
|
|
|
|
|
|
mac = uuid.getnode()
|
|
|
|
|
|
mac_str = ':'.join(['{:02x}'.format((mac >> i) & 0xff) for i in range(0, 48, 8)][::-1])
|
|
|
|
|
|
return hashlib.md5(mac_str.encode()).hexdigest()[:16].upper()
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
# 最终备用
|
|
|
|
|
|
fallback = f"{platform.node()}{uuid.getnode()}"
|
|
|
|
|
|
return hashlib.md5(fallback.encode()).hexdigest()[:16].upper()
|
|
|
|
|
|
|
2025-10-25 17:49:09 +08:00
|
|
|
|
# ========== 持久化缓存管理器 ==========
|
|
|
|
|
|
class LicenseCache:
|
|
|
|
|
|
"""持久化许可证缓存管理器(使用SQLite)"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
# 存储在用户AppData目录,不会被系统清理
|
|
|
|
|
|
if os.name == 'nt': # Windows
|
|
|
|
|
|
cache_base = Path(os.environ.get('APPDATA', Path.home()))
|
|
|
|
|
|
else: # Linux/Mac
|
|
|
|
|
|
cache_base = Path.home() / '.config'
|
2025-10-23 18:28:10 +08:00
|
|
|
|
|
2025-10-25 17:49:09 +08:00
|
|
|
|
self.cache_dir = cache_base / '.secure_license'
|
|
|
|
|
|
self.cache_dir.mkdir(exist_ok=True, parents=True)
|
|
|
|
|
|
self.db_path = self.cache_dir / 'license.db'
|
|
|
|
|
|
self._init_db()
|
|
|
|
|
|
|
|
|
|
|
|
def _init_db(self):
|
|
|
|
|
|
"""初始化数据库"""
|
|
|
|
|
|
conn = sqlite3.connect(str(self.db_path))
|
|
|
|
|
|
conn.execute('''
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS license_cache (
|
|
|
|
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
|
|
software_name TEXT NOT NULL,
|
|
|
|
|
|
license_key TEXT NOT NULL,
|
|
|
|
|
|
machine_code TEXT NOT NULL,
|
|
|
|
|
|
token TEXT NOT NULL,
|
|
|
|
|
|
start_time TEXT NOT NULL,
|
|
|
|
|
|
end_time TEXT NOT NULL,
|
|
|
|
|
|
last_validated TEXT NOT NULL,
|
|
|
|
|
|
status TEXT DEFAULT 'active',
|
|
|
|
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
|
|
UNIQUE(software_name, machine_code)
|
|
|
|
|
|
)
|
|
|
|
|
|
''')
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
|
|
|
|
|
|
def save_license(self, software_name, license_key, machine_code,
|
|
|
|
|
|
token, start_time, end_time):
|
|
|
|
|
|
"""保存许可证信息"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
conn = sqlite3.connect(str(self.db_path))
|
|
|
|
|
|
# 转换datetime为ISO格式字符串
|
|
|
|
|
|
if isinstance(start_time, datetime):
|
|
|
|
|
|
start_time = start_time.isoformat()
|
|
|
|
|
|
if isinstance(end_time, datetime):
|
|
|
|
|
|
end_time = end_time.isoformat()
|
2025-10-23 18:28:10 +08:00
|
|
|
|
|
2025-10-25 17:49:09 +08:00
|
|
|
|
conn.execute('''
|
|
|
|
|
|
INSERT OR REPLACE INTO license_cache
|
|
|
|
|
|
(software_name, license_key, machine_code, token,
|
|
|
|
|
|
start_time, end_time, last_validated)
|
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
|
|
|
|
''', (software_name, license_key, machine_code, token,
|
|
|
|
|
|
start_time, end_time, datetime.now().isoformat()))
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"保存许可证缓存失败: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def get_license(self, software_name, machine_code):
|
|
|
|
|
|
"""获取许可证信息"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
conn = sqlite3.connect(str(self.db_path))
|
|
|
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
cursor.execute('''
|
|
|
|
|
|
SELECT * FROM license_cache
|
|
|
|
|
|
WHERE software_name=? AND machine_code=?
|
|
|
|
|
|
''', (software_name, machine_code))
|
|
|
|
|
|
row = cursor.fetchone()
|
|
|
|
|
|
|
|
|
|
|
|
if not row:
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# 转换为字典
|
|
|
|
|
|
columns = [desc[0] for desc in cursor.description]
|
|
|
|
|
|
license_data = dict(zip(columns, row))
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
|
|
|
|
|
|
return license_data
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"读取许可证缓存失败: {e}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def should_revalidate(self, license_data):
|
|
|
|
|
|
"""判断是否需要重新验证(每7天联网验证一次)"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
last_validated = datetime.fromisoformat(license_data['last_validated'])
|
|
|
|
|
|
return (datetime.now() - last_validated).days >= 7
|
|
|
|
|
|
except:
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def is_expired(self, license_data):
|
|
|
|
|
|
"""检查许可证是否过期"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
end_time = datetime.fromisoformat(license_data['end_time'])
|
|
|
|
|
|
return datetime.now() > end_time
|
|
|
|
|
|
except:
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def get_days_remaining(self, license_data):
|
|
|
|
|
|
"""获取剩余天数"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
end_time = datetime.fromisoformat(license_data['end_time'])
|
|
|
|
|
|
remaining = (end_time - datetime.now()).days
|
|
|
|
|
|
return max(0, remaining)
|
|
|
|
|
|
except:
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
def update_last_validated(self, software_name, machine_code):
|
|
|
|
|
|
"""更新最后验证时间"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
conn = sqlite3.connect(str(self.db_path))
|
|
|
|
|
|
conn.execute('''
|
|
|
|
|
|
UPDATE license_cache
|
|
|
|
|
|
SET last_validated=?
|
|
|
|
|
|
WHERE software_name=? AND machine_code=?
|
|
|
|
|
|
''', (datetime.now().isoformat(), software_name, machine_code))
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"更新验证时间失败: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def clear_license(self, software_name, machine_code):
|
|
|
|
|
|
"""清除许可证缓存"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
conn = sqlite3.connect(str(self.db_path))
|
|
|
|
|
|
conn.execute('''
|
|
|
|
|
|
DELETE FROM license_cache
|
|
|
|
|
|
WHERE software_name=? AND machine_code=?
|
|
|
|
|
|
''', (software_name, machine_code))
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"清除许可证缓存失败: {e}")
|
|
|
|
|
|
return False
|
2025-10-23 18:28:10 +08:00
|
|
|
|
|
|
|
|
|
|
# ========== API通信 ==========
|
|
|
|
|
|
def create_signature(license_key, machine_code, software_name):
|
|
|
|
|
|
"""创建请求签名"""
|
|
|
|
|
|
message = f"{license_key}{machine_code}{software_name}"
|
|
|
|
|
|
return hmac.new(API_KEY.encode(), message.encode(), hashlib.sha256).hexdigest()
|
|
|
|
|
|
|
|
|
|
|
|
def validate_license_online(license_key, machine_code):
|
|
|
|
|
|
"""在线验证卡密"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
signature = create_signature(license_key, machine_code, SOFTWARE_NAME)
|
|
|
|
|
|
|
|
|
|
|
|
response = requests.post(
|
2025-10-25 17:49:09 +08:00
|
|
|
|
f"{API_BASE_URL}api/validate",
|
2025-10-23 18:28:10 +08:00
|
|
|
|
json={
|
|
|
|
|
|
'license_key': license_key,
|
|
|
|
|
|
'machine_code': machine_code,
|
|
|
|
|
|
'software_name': SOFTWARE_NAME,
|
|
|
|
|
|
'signature': signature
|
|
|
|
|
|
},
|
|
|
|
|
|
headers={'X-API-Key': API_KEY},
|
2025-10-25 17:49:09 +08:00
|
|
|
|
timeout=10,
|
|
|
|
|
|
verify=False # 跳过SSL证书验证(用于自签名证书)
|
2025-10-23 18:28:10 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if response.status_code == 200:
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
|
if data.get('success'):
|
2025-10-25 17:49:09 +08:00
|
|
|
|
# 保存到持久化缓存
|
|
|
|
|
|
cache = LicenseCache()
|
|
|
|
|
|
# 获取服务器返回的时间信息(如果有)
|
|
|
|
|
|
end_time = data.get('expires_at', (datetime.now() + timedelta(days=30)).isoformat())
|
|
|
|
|
|
start_time = datetime.now().isoformat()
|
|
|
|
|
|
|
|
|
|
|
|
cache.save_license(
|
|
|
|
|
|
SOFTWARE_NAME,
|
2025-10-23 18:28:10 +08:00
|
|
|
|
license_key,
|
2025-10-25 17:49:09 +08:00
|
|
|
|
machine_code,
|
2025-10-23 18:28:10 +08:00
|
|
|
|
data.get('token', ''),
|
2025-10-25 17:49:09 +08:00
|
|
|
|
start_time,
|
|
|
|
|
|
end_time
|
2025-10-23 18:28:10 +08:00
|
|
|
|
)
|
|
|
|
|
|
return True, data.get('message', '验证成功')
|
|
|
|
|
|
else:
|
|
|
|
|
|
return False, data.get('message', '验证失败')
|
|
|
|
|
|
else:
|
|
|
|
|
|
return False, f"服务器错误: {response.status_code}"
|
|
|
|
|
|
|
|
|
|
|
|
except requests.exceptions.ConnectionError:
|
|
|
|
|
|
return False, "无法连接到服务器,请检查网络"
|
|
|
|
|
|
except requests.exceptions.Timeout:
|
|
|
|
|
|
return False, "服务器响应超时"
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return False, f"验证过程出错: {str(e)}"
|
|
|
|
|
|
|
2025-10-25 17:49:09 +08:00
|
|
|
|
def validate_license_offline(license_data):
|
|
|
|
|
|
"""增强的离线验证"""
|
2025-10-23 18:28:10 +08:00
|
|
|
|
try:
|
2025-10-25 17:49:09 +08:00
|
|
|
|
# 1. 验证数据完整性
|
|
|
|
|
|
required_fields = ['license_key', 'token', 'end_time', 'machine_code']
|
|
|
|
|
|
if not all(field in license_data for field in required_fields):
|
|
|
|
|
|
return False, "缓存数据不完整", None
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 验证机器码
|
|
|
|
|
|
current_machine_code = get_machine_code()
|
|
|
|
|
|
if license_data['machine_code'] != current_machine_code:
|
|
|
|
|
|
return False, "设备已更改,需要重新激活", None
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 检查许可证是否过期
|
|
|
|
|
|
end_time = datetime.fromisoformat(license_data['end_time'])
|
|
|
|
|
|
if datetime.now() > end_time:
|
|
|
|
|
|
return False, "许可证已过期", None
|
2025-10-23 18:28:10 +08:00
|
|
|
|
|
2025-10-25 17:49:09 +08:00
|
|
|
|
# 4. 计算剩余天数
|
|
|
|
|
|
remaining_days = (end_time - datetime.now()).days
|
|
|
|
|
|
|
|
|
|
|
|
# 5. 验证token(基本的完整性检查)
|
|
|
|
|
|
token = license_data.get('token', '')
|
|
|
|
|
|
if not token or len(token) < 10:
|
|
|
|
|
|
return False, "许可证数据异常", None
|
|
|
|
|
|
|
|
|
|
|
|
# 6. 返回详细信息
|
|
|
|
|
|
info = {
|
|
|
|
|
|
'remaining_days': remaining_days,
|
|
|
|
|
|
'end_time': end_time,
|
|
|
|
|
|
'license_key': license_data['license_key']
|
|
|
|
|
|
}
|
2025-10-23 18:28:10 +08:00
|
|
|
|
|
2025-10-25 17:49:09 +08:00
|
|
|
|
return True, f"离线验证成功", info
|
2025-10-23 18:28:10 +08:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-10-25 17:49:09 +08:00
|
|
|
|
return False, f"离线验证失败: {str(e)}", None
|
|
|
|
|
|
|
|
|
|
|
|
def try_online_revalidation(license_data):
|
|
|
|
|
|
"""尝试联网刷新状态(静默,不打扰用户)"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
machine_code = get_machine_code()
|
|
|
|
|
|
success, msg = validate_license_online(
|
|
|
|
|
|
license_data['license_key'],
|
|
|
|
|
|
machine_code
|
|
|
|
|
|
)
|
|
|
|
|
|
return success, msg
|
|
|
|
|
|
except:
|
|
|
|
|
|
# 网络问题,不影响使用
|
|
|
|
|
|
return True, "离线模式"
|
|
|
|
|
|
|
|
|
|
|
|
# ========== 到期提醒 ==========
|
|
|
|
|
|
def show_expiry_reminder(remaining_days, end_time_str):
|
|
|
|
|
|
"""显示到期提醒"""
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
end_time = datetime.fromisoformat(end_time_str)
|
|
|
|
|
|
except:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 根据剩余天数选择不同的提醒级别
|
|
|
|
|
|
if remaining_days <= 0:
|
|
|
|
|
|
# 已过期,必须续费
|
|
|
|
|
|
title = "⚠️ 许可证已过期"
|
|
|
|
|
|
message = f"您的许可证已于 {end_time.strftime('%Y-%m-%d')} 到期\n请联系管理员续费"
|
|
|
|
|
|
urgency = "critical"
|
|
|
|
|
|
elif remaining_days == 1:
|
|
|
|
|
|
title = "🔴 许可证即将到期"
|
|
|
|
|
|
message = f"您的许可证将于明天到期\n请尽快联系管理员续费"
|
|
|
|
|
|
urgency = "urgent"
|
|
|
|
|
|
elif remaining_days <= 3:
|
|
|
|
|
|
title = "🟡 许可证即将到期"
|
|
|
|
|
|
message = f"您的许可证还剩 {remaining_days} 天\n建议尽快联系管理员续费"
|
|
|
|
|
|
urgency = "warning"
|
|
|
|
|
|
elif remaining_days <= 7:
|
|
|
|
|
|
title = "🟢 许可证到期提醒"
|
|
|
|
|
|
message = f"您的许可证还剩 {remaining_days} 天\n请注意及时续费"
|
|
|
|
|
|
urgency = "info"
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 不需要提醒
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 创建提醒窗口
|
|
|
|
|
|
root = tk.Tk()
|
|
|
|
|
|
root.title(title)
|
|
|
|
|
|
root.geometry("450x280")
|
|
|
|
|
|
root.resizable(False, False)
|
|
|
|
|
|
|
|
|
|
|
|
# 居中
|
|
|
|
|
|
root.update_idletasks()
|
|
|
|
|
|
x = (root.winfo_screenwidth() - 450) // 2
|
|
|
|
|
|
y = (root.winfo_screenheight() - 280) // 2
|
|
|
|
|
|
root.geometry(f"450x280+{x}+{y}")
|
|
|
|
|
|
|
|
|
|
|
|
# 根据紧急程度设置窗口属性
|
|
|
|
|
|
if urgency == "critical":
|
|
|
|
|
|
root.attributes('-topmost', True)
|
|
|
|
|
|
|
|
|
|
|
|
# 标题
|
|
|
|
|
|
title_color = "red" if urgency in ["critical", "urgent"] else "orange" if urgency == "warning" else "green"
|
|
|
|
|
|
tk.Label(root, text=title,
|
|
|
|
|
|
font=("Microsoft YaHei", 14, "bold"),
|
|
|
|
|
|
fg=title_color).pack(pady=20)
|
|
|
|
|
|
|
|
|
|
|
|
# 消息
|
|
|
|
|
|
tk.Label(root, text=message,
|
|
|
|
|
|
font=("Microsoft YaHei", 11),
|
|
|
|
|
|
justify=tk.CENTER).pack(pady=10)
|
|
|
|
|
|
|
|
|
|
|
|
# 到期时间
|
|
|
|
|
|
tk.Label(root, text=f"到期时间:{end_time.strftime('%Y年%m月%d日')}",
|
|
|
|
|
|
font=("Microsoft YaHei", 10),
|
|
|
|
|
|
fg="gray").pack(pady=5)
|
|
|
|
|
|
|
|
|
|
|
|
# 剩余天数(醒目显示)
|
|
|
|
|
|
if remaining_days > 0:
|
|
|
|
|
|
tk.Label(root, text=f"剩余 {remaining_days} 天",
|
|
|
|
|
|
font=("Microsoft YaHei", 16, "bold"),
|
|
|
|
|
|
fg=title_color).pack(pady=10)
|
|
|
|
|
|
|
|
|
|
|
|
# 联系方式
|
|
|
|
|
|
tk.Label(root, text="联系管理员续费:V:taiyi1224",
|
|
|
|
|
|
font=("Microsoft YaHei", 9),
|
|
|
|
|
|
fg="blue").pack(pady=10)
|
|
|
|
|
|
|
|
|
|
|
|
# 按钮
|
|
|
|
|
|
frame_buttons = tk.Frame(root)
|
|
|
|
|
|
frame_buttons.pack(pady=15)
|
|
|
|
|
|
|
|
|
|
|
|
def on_continue():
|
|
|
|
|
|
root.destroy()
|
|
|
|
|
|
|
|
|
|
|
|
def on_contact():
|
|
|
|
|
|
import webbrowser
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 尝试打开微信(如果有协议链接)
|
|
|
|
|
|
webbrowser.open("weixin://")
|
|
|
|
|
|
except:
|
|
|
|
|
|
pass
|
|
|
|
|
|
root.destroy()
|
|
|
|
|
|
|
|
|
|
|
|
# 如果未过期,显示"继续使用"按钮
|
|
|
|
|
|
if urgency != "critical":
|
|
|
|
|
|
tk.Button(frame_buttons, text="继续使用", command=on_continue,
|
|
|
|
|
|
bg="#27ae60", fg="white", font=("Microsoft YaHei", 10, "bold"),
|
|
|
|
|
|
padx=20, pady=8).pack(side=tk.LEFT, padx=10)
|
|
|
|
|
|
|
|
|
|
|
|
tk.Button(frame_buttons, text="联系续费", command=on_contact,
|
|
|
|
|
|
bg="#3498db", fg="white", font=("Microsoft YaHei", 10, "bold"),
|
|
|
|
|
|
padx=20, pady=8).pack(side=tk.LEFT, padx=10)
|
|
|
|
|
|
|
|
|
|
|
|
# 如果是critical级别,设置为模态窗口
|
|
|
|
|
|
if urgency == "critical":
|
|
|
|
|
|
root.transient()
|
|
|
|
|
|
root.grab_set()
|
|
|
|
|
|
|
|
|
|
|
|
root.mainloop()
|
2025-10-23 18:28:10 +08:00
|
|
|
|
|
|
|
|
|
|
# ========== 资源解密 ==========
|
|
|
|
|
|
# ENCRYPTED_RESOURCE_PLACEHOLDER
|
|
|
|
|
|
ENCRYPTED_RESOURCE = None
|
|
|
|
|
|
|
|
|
|
|
|
def decrypt_resource():
|
|
|
|
|
|
"""解密嵌入的资源"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if not ENCRYPTED_RESOURCE:
|
|
|
|
|
|
return None, "未找到嵌入资源"
|
|
|
|
|
|
|
|
|
|
|
|
from cryptography.fernet import Fernet
|
|
|
|
|
|
import base64
|
|
|
|
|
|
|
|
|
|
|
|
# 重建完整密钥
|
|
|
|
|
|
key_part1_bytes = base64.b64decode(ENCRYPTED_RESOURCE['key_hint'].encode())
|
|
|
|
|
|
key_part2_bytes = base64.b64decode(ENCRYPTED_RESOURCE['key_part2'].encode())
|
|
|
|
|
|
|
|
|
|
|
|
# 合并密钥
|
|
|
|
|
|
full_key = key_part1_bytes + key_part2_bytes
|
|
|
|
|
|
|
|
|
|
|
|
# 解密
|
|
|
|
|
|
fernet = Fernet(full_key)
|
|
|
|
|
|
encrypted_data = base64.b64decode(ENCRYPTED_RESOURCE['data'].encode())
|
|
|
|
|
|
decrypted = fernet.decrypt(encrypted_data)
|
|
|
|
|
|
|
|
|
|
|
|
# 验证哈希
|
|
|
|
|
|
if hashlib.sha256(decrypted).hexdigest() != ENCRYPTED_RESOURCE['hash']:
|
|
|
|
|
|
return None, "文件完整性校验失败"
|
|
|
|
|
|
|
|
|
|
|
|
return decrypted, "解密成功"
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return None, f"解密失败: {str(e)}"
|
|
|
|
|
|
|
|
|
|
|
|
def extract_and_run():
|
|
|
|
|
|
"""提取并运行原始程序"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
exe_content, msg = decrypt_resource()
|
|
|
|
|
|
if not exe_content:
|
|
|
|
|
|
return False, msg
|
|
|
|
|
|
|
|
|
|
|
|
# 写入临时文件
|
|
|
|
|
|
temp_path = os.path.join(tempfile.gettempdir(), f"app_{os.getpid()}.exe")
|
|
|
|
|
|
with open(temp_path, 'wb') as f:
|
|
|
|
|
|
f.write(exe_content)
|
|
|
|
|
|
|
|
|
|
|
|
# 设置权限
|
|
|
|
|
|
if hasattr(os, 'chmod'):
|
|
|
|
|
|
os.chmod(temp_path, 0o755)
|
|
|
|
|
|
|
|
|
|
|
|
# 启动程序
|
|
|
|
|
|
if os.name == 'nt':
|
|
|
|
|
|
subprocess.Popen([temp_path], creationflags=subprocess.CREATE_NEW_CONSOLE)
|
|
|
|
|
|
else:
|
|
|
|
|
|
subprocess.Popen([temp_path])
|
|
|
|
|
|
|
|
|
|
|
|
time.sleep(2) # 等待程序启动
|
|
|
|
|
|
|
|
|
|
|
|
return True, "程序启动成功"
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return False, f"启动失败: {str(e)}"
|
|
|
|
|
|
|
2025-10-25 17:49:09 +08:00
|
|
|
|
# ========== 增强的激活对话框 ==========
|
|
|
|
|
|
def show_enhanced_activation_dialog(message=None):
|
|
|
|
|
|
"""增强版激活对话框"""
|
2025-10-23 18:28:10 +08:00
|
|
|
|
|
2025-10-25 17:49:09 +08:00
|
|
|
|
# 尝试加载已有许可证信息
|
|
|
|
|
|
cache = LicenseCache()
|
2025-10-23 18:28:10 +08:00
|
|
|
|
machine_code = get_machine_code()
|
2025-10-25 17:49:09 +08:00
|
|
|
|
existing_license = cache.get_license(SOFTWARE_NAME, machine_code)
|
2025-10-23 18:28:10 +08:00
|
|
|
|
|
|
|
|
|
|
root = tk.Tk()
|
2025-10-25 17:49:09 +08:00
|
|
|
|
root.title(f"{SOFTWARE_NAME} - 许可证管理")
|
|
|
|
|
|
root.geometry("550x520")
|
2025-10-23 18:28:10 +08:00
|
|
|
|
root.resizable(False, False)
|
|
|
|
|
|
|
|
|
|
|
|
# 居中
|
|
|
|
|
|
root.update_idletasks()
|
2025-10-25 17:49:09 +08:00
|
|
|
|
x = (root.winfo_screenwidth() - 550) // 2
|
|
|
|
|
|
y = (root.winfo_screenheight() - 520) // 2
|
|
|
|
|
|
root.geometry(f"550x520+{x}+{y}")
|
2025-10-23 18:28:10 +08:00
|
|
|
|
|
|
|
|
|
|
# 标题
|
|
|
|
|
|
tk.Label(root, text="🔐 软件许可证验证",
|
|
|
|
|
|
font=("Microsoft YaHei", 16, "bold")).pack(pady=20)
|
|
|
|
|
|
|
2025-10-25 17:49:09 +08:00
|
|
|
|
# 如果有消息,显示
|
|
|
|
|
|
if message:
|
|
|
|
|
|
tk.Label(root, text=message,
|
|
|
|
|
|
font=("Microsoft YaHei", 10),
|
|
|
|
|
|
fg="red").pack(pady=5)
|
|
|
|
|
|
|
|
|
|
|
|
# 如果有现有许可证,显示状态
|
|
|
|
|
|
if existing_license:
|
|
|
|
|
|
status_frame = tk.LabelFrame(root, text="当前许可证状态",
|
|
|
|
|
|
font=("Microsoft YaHei", 10, "bold"),
|
|
|
|
|
|
padx=10, pady=10)
|
|
|
|
|
|
status_frame.pack(fill=tk.X, padx=30, pady=10)
|
|
|
|
|
|
|
|
|
|
|
|
remaining = cache.get_days_remaining(existing_license)
|
|
|
|
|
|
is_expired = cache.is_expired(existing_license)
|
|
|
|
|
|
|
|
|
|
|
|
if is_expired:
|
|
|
|
|
|
status_text = "❌ 已过期"
|
|
|
|
|
|
status_color = "red"
|
|
|
|
|
|
elif remaining <= 7:
|
|
|
|
|
|
status_text = f"⚠️ 剩余 {remaining} 天"
|
|
|
|
|
|
status_color = "orange"
|
|
|
|
|
|
else:
|
|
|
|
|
|
status_text = f"✅ 剩余 {remaining} 天"
|
|
|
|
|
|
status_color = "green"
|
|
|
|
|
|
|
|
|
|
|
|
tk.Label(status_frame, text=f"状态:{status_text}",
|
|
|
|
|
|
font=("Microsoft YaHei", 11, "bold"),
|
|
|
|
|
|
fg=status_color).pack(anchor=tk.W, pady=3)
|
|
|
|
|
|
|
|
|
|
|
|
tk.Label(status_frame,
|
|
|
|
|
|
text=f"激活码:{existing_license['license_key']}",
|
|
|
|
|
|
font=("Consolas", 9),
|
|
|
|
|
|
fg="gray").pack(anchor=tk.W, pady=2)
|
|
|
|
|
|
|
|
|
|
|
|
end_time_str = existing_license.get('end_time', '')
|
|
|
|
|
|
if end_time_str:
|
|
|
|
|
|
try:
|
|
|
|
|
|
end_time = datetime.fromisoformat(end_time_str)
|
|
|
|
|
|
tk.Label(status_frame,
|
|
|
|
|
|
text=f"到期时间:{end_time.strftime('%Y年%m月%d日')}",
|
|
|
|
|
|
font=("Microsoft YaHei", 9),
|
|
|
|
|
|
fg="gray").pack(anchor=tk.W, pady=2)
|
|
|
|
|
|
except:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
2025-10-23 18:28:10 +08:00
|
|
|
|
# 机器码
|
|
|
|
|
|
frame_machine = tk.Frame(root)
|
|
|
|
|
|
frame_machine.pack(fill=tk.X, padx=30, pady=10)
|
2025-10-25 17:49:09 +08:00
|
|
|
|
tk.Label(frame_machine, text="机器码:",
|
|
|
|
|
|
font=("Microsoft YaHei", 10, "bold")).pack(anchor=tk.W)
|
|
|
|
|
|
entry_machine = tk.Entry(frame_machine, state="readonly",
|
|
|
|
|
|
font=("Consolas", 10),
|
|
|
|
|
|
justify=tk.CENTER)
|
2025-10-23 18:28:10 +08:00
|
|
|
|
entry_machine.insert(0, machine_code)
|
|
|
|
|
|
entry_machine.pack(fill=tk.X, pady=5)
|
|
|
|
|
|
|
|
|
|
|
|
def copy_machine_code():
|
|
|
|
|
|
root.clipboard_clear()
|
|
|
|
|
|
root.clipboard_append(machine_code)
|
2025-10-25 17:49:09 +08:00
|
|
|
|
messagebox.showinfo("成功", "机器码已复制到剪贴板\n请发送给管理员获取激活码")
|
2025-10-23 18:28:10 +08:00
|
|
|
|
|
2025-10-25 17:49:09 +08:00
|
|
|
|
tk.Button(frame_machine, text="📋 复制机器码",
|
|
|
|
|
|
command=copy_machine_code,
|
|
|
|
|
|
font=("Microsoft YaHei", 9)).pack(anchor=tk.E)
|
2025-10-23 18:28:10 +08:00
|
|
|
|
|
2025-10-25 17:49:09 +08:00
|
|
|
|
# 激活码输入
|
2025-10-23 18:28:10 +08:00
|
|
|
|
frame_key = tk.Frame(root)
|
|
|
|
|
|
frame_key.pack(fill=tk.X, padx=30, pady=10)
|
2025-10-25 17:49:09 +08:00
|
|
|
|
tk.Label(frame_key, text="激活码:" if not existing_license else "新激活码:",
|
|
|
|
|
|
font=("Microsoft YaHei", 10, "bold")).pack(anchor=tk.W)
|
|
|
|
|
|
entry_key = tk.Entry(frame_key, font=("Consolas", 12),
|
|
|
|
|
|
justify=tk.CENTER)
|
2025-10-23 18:28:10 +08:00
|
|
|
|
entry_key.pack(fill=tk.X, pady=5)
|
|
|
|
|
|
entry_key.focus()
|
|
|
|
|
|
|
2025-10-25 17:49:09 +08:00
|
|
|
|
# 状态标签
|
2025-10-23 18:28:10 +08:00
|
|
|
|
status_label = tk.Label(root, text="", font=("Microsoft YaHei", 9))
|
|
|
|
|
|
status_label.pack(pady=10)
|
|
|
|
|
|
|
2025-10-25 17:49:09 +08:00
|
|
|
|
# 按钮区域
|
2025-10-23 18:28:10 +08:00
|
|
|
|
frame_buttons = tk.Frame(root)
|
2025-10-25 17:49:09 +08:00
|
|
|
|
frame_buttons.pack(pady=15)
|
|
|
|
|
|
|
|
|
|
|
|
def on_activate():
|
|
|
|
|
|
license_key = entry_key.get().strip()
|
|
|
|
|
|
if not license_key:
|
|
|
|
|
|
messagebox.showerror("错误", "请输入激活码")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
btn_activate.config(state=tk.DISABLED, text="验证中...")
|
|
|
|
|
|
status_label.config(text="正在联网验证激活码,请稍候...", fg="blue")
|
|
|
|
|
|
root.update()
|
|
|
|
|
|
|
|
|
|
|
|
def validate_thread():
|
|
|
|
|
|
success, msg = validate_license_online(license_key, machine_code)
|
|
|
|
|
|
root.after(0, lambda: handle_result(success, msg))
|
|
|
|
|
|
|
|
|
|
|
|
threading.Thread(target=validate_thread, daemon=True).start()
|
2025-10-23 18:28:10 +08:00
|
|
|
|
|
2025-10-25 17:49:09 +08:00
|
|
|
|
def handle_result(success, msg):
|
|
|
|
|
|
if success:
|
|
|
|
|
|
messagebox.showinfo("成功", f"激活成功!\n{msg}\n\n正在启动程序...")
|
|
|
|
|
|
root.destroy()
|
|
|
|
|
|
ok, err = extract_and_run()
|
|
|
|
|
|
if ok:
|
|
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
else:
|
|
|
|
|
|
messagebox.showerror("错误", f"程序启动失败:\n{err}")
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
else:
|
|
|
|
|
|
messagebox.showerror("激活失败", f"{msg}\n\n请检查:\n1. 激活码是否正确\n2. 网络连接是否正常\n3. 激活码是否已被其他设备使用")
|
|
|
|
|
|
btn_activate.config(state=tk.NORMAL, text="验证并激活")
|
|
|
|
|
|
status_label.config(text="", fg="black")
|
|
|
|
|
|
|
|
|
|
|
|
btn_activate = tk.Button(frame_buttons, text="验证并激活",
|
|
|
|
|
|
command=on_activate,
|
|
|
|
|
|
bg="#27ae60", fg="white",
|
|
|
|
|
|
font=("Microsoft YaHei", 11, "bold"),
|
|
|
|
|
|
padx=25, pady=10,
|
|
|
|
|
|
cursor="hand2")
|
2025-10-23 18:28:10 +08:00
|
|
|
|
btn_activate.pack(side=tk.LEFT, padx=10)
|
|
|
|
|
|
|
2025-10-25 17:49:09 +08:00
|
|
|
|
# 如果有现有许可证且未过期,允许继续使用
|
|
|
|
|
|
if existing_license and not cache.is_expired(existing_license):
|
|
|
|
|
|
def on_continue():
|
|
|
|
|
|
root.destroy()
|
|
|
|
|
|
ok, err = extract_and_run()
|
|
|
|
|
|
if ok:
|
|
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
else:
|
|
|
|
|
|
messagebox.showerror("错误", f"程序启动失败:\n{err}")
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
tk.Button(frame_buttons, text="继续使用", command=on_continue,
|
|
|
|
|
|
bg="#3498db", fg="white",
|
|
|
|
|
|
font=("Microsoft YaHei", 11, "bold"),
|
|
|
|
|
|
padx=25, pady=10,
|
|
|
|
|
|
cursor="hand2").pack(side=tk.LEFT, padx=10)
|
|
|
|
|
|
|
2025-10-23 18:28:10 +08:00
|
|
|
|
tk.Button(frame_buttons, text="退出", command=sys.exit,
|
2025-10-25 17:49:09 +08:00
|
|
|
|
bg="#e74c3c", fg="white",
|
|
|
|
|
|
font=("Microsoft YaHei", 11, "bold"),
|
|
|
|
|
|
padx=25, pady=10,
|
|
|
|
|
|
cursor="hand2").pack(side=tk.LEFT, padx=10)
|
|
|
|
|
|
|
|
|
|
|
|
# 帮助信息
|
|
|
|
|
|
help_frame = tk.Frame(root)
|
|
|
|
|
|
help_frame.pack(pady=10)
|
|
|
|
|
|
|
|
|
|
|
|
tk.Label(help_frame, text="需要帮助?",
|
|
|
|
|
|
font=("Microsoft YaHei", 8),
|
|
|
|
|
|
fg="gray").pack()
|
|
|
|
|
|
tk.Label(help_frame, text="联系管理员:V:taiyi1224",
|
|
|
|
|
|
font=("Microsoft YaHei", 8, "bold"),
|
|
|
|
|
|
fg="blue",
|
|
|
|
|
|
cursor="hand2").pack()
|
2025-10-23 18:28:10 +08:00
|
|
|
|
|
|
|
|
|
|
root.bind('<Return>', lambda e: on_activate())
|
|
|
|
|
|
root.protocol("WM_DELETE_WINDOW", sys.exit)
|
|
|
|
|
|
root.mainloop()
|
|
|
|
|
|
|
2025-10-25 17:49:09 +08:00
|
|
|
|
# ========== 智能验证主流程 ==========
|
|
|
|
|
|
def smart_validate():
|
|
|
|
|
|
"""智能验证策略"""
|
2025-10-23 18:28:10 +08:00
|
|
|
|
|
2025-10-25 17:49:09 +08:00
|
|
|
|
try:
|
|
|
|
|
|
# 1. 初始化缓存管理器
|
|
|
|
|
|
cache = LicenseCache()
|
|
|
|
|
|
machine_code = get_machine_code()
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 尝试加载本地缓存
|
|
|
|
|
|
license_data = cache.get_license(SOFTWARE_NAME, machine_code)
|
|
|
|
|
|
|
|
|
|
|
|
if not license_data:
|
|
|
|
|
|
# 首次使用,需要在线激活
|
|
|
|
|
|
print("首次使用,需要激活")
|
|
|
|
|
|
show_enhanced_activation_dialog()
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 检查是否过期
|
|
|
|
|
|
if cache.is_expired(license_data):
|
|
|
|
|
|
print("许可证已过期")
|
|
|
|
|
|
cache.clear_license(SOFTWARE_NAME, machine_code)
|
|
|
|
|
|
show_enhanced_activation_dialog(message="许可证已过期,请重新激活")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 4. 计算剩余天数
|
|
|
|
|
|
remaining_days = cache.get_days_remaining(license_data)
|
|
|
|
|
|
print(f"许可证剩余 {remaining_days} 天")
|
|
|
|
|
|
|
|
|
|
|
|
# 5. 判断是否需要联网刷新(每7天一次)
|
|
|
|
|
|
if cache.should_revalidate(license_data):
|
|
|
|
|
|
print("尝试联网刷新状态...")
|
|
|
|
|
|
# 尝试联网验证(静默,不打扰用户)
|
|
|
|
|
|
success, msg = try_online_revalidation(license_data)
|
|
|
|
|
|
if success:
|
|
|
|
|
|
# 更新缓存中的验证时间
|
|
|
|
|
|
cache.update_last_validated(SOFTWARE_NAME, machine_code)
|
|
|
|
|
|
print(f"状态刷新成功: {msg}")
|
2025-10-23 18:28:10 +08:00
|
|
|
|
else:
|
2025-10-25 17:49:09 +08:00
|
|
|
|
print(f"状态刷新失败(不影响使用): {msg}")
|
|
|
|
|
|
# 即使失败也继续使用(网络问题不影响已激活用户)
|
|
|
|
|
|
|
|
|
|
|
|
# 6. 离线验证
|
|
|
|
|
|
success, msg, info = validate_license_offline(license_data)
|
|
|
|
|
|
if not success:
|
2025-10-23 18:28:10 +08:00
|
|
|
|
print(f"离线验证失败: {msg}")
|
2025-10-25 17:49:09 +08:00
|
|
|
|
cache.clear_license(SOFTWARE_NAME, machine_code)
|
|
|
|
|
|
show_enhanced_activation_dialog(message=msg)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
print(f"离线验证成功: {msg}")
|
|
|
|
|
|
|
|
|
|
|
|
# 7. 显示到期提醒(如果接近到期)
|
|
|
|
|
|
if remaining_days <= 7:
|
|
|
|
|
|
print(f"显示到期提醒(剩余{remaining_days}天)")
|
|
|
|
|
|
show_expiry_reminder(remaining_days, license_data['end_time'])
|
|
|
|
|
|
|
|
|
|
|
|
# 8. 验证通过,启动程序
|
|
|
|
|
|
print("启动程序...")
|
|
|
|
|
|
ok, err = extract_and_run()
|
|
|
|
|
|
if ok:
|
|
|
|
|
|
print("程序启动成功")
|
|
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"程序启动失败: {err}")
|
|
|
|
|
|
messagebox.showerror("错误", f"程序启动失败:\n{err}")
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"验证过程出错: {e}")
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
messagebox.showerror("错误", f"验证过程出错:\n{str(e)}")
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
# ========== 主函数 ==========
|
|
|
|
|
|
def main():
|
|
|
|
|
|
"""主函数"""
|
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
|
print(f"软件许可证验证系统 V2.0")
|
|
|
|
|
|
print(f"软件名称: {SOFTWARE_NAME}")
|
|
|
|
|
|
print(f"机器码: {get_machine_code()}")
|
|
|
|
|
|
print("=" * 60)
|
2025-10-23 18:28:10 +08:00
|
|
|
|
|
2025-10-25 17:49:09 +08:00
|
|
|
|
# 使用智能验证策略
|
|
|
|
|
|
smart_validate()
|
2025-10-23 18:28:10 +08:00
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
|
main()
|
|
|
|
|
|
|