文档版本: v1.3.0 创建日期: 2026-02-01 最后更新: 2026-02-02 文档目标: 总结提炼数据库模型和客户端exe建设思路,形成完整技术方案
v1.3.0 更新内容:
- 新增 4.4 接口日志记录规范
- 记录入参密文、入参明文、出参明文、出参密文
- 日志文件按天滚动,每天一个日志文件
- 敏感字段自动脱敏
- 支持文件日志、数据库日志、控制台日志三种输出方式
v1.2.1 更新内容:
- 优化机器码生成方案,仅使用硬盘序列号和MAC地址
- 移除主板序列号依赖,提高云环境兼容性
- 解决云虚拟机主板序列号不可用的问题
v1.2.0 更新内容:
- 新增 3.3.1 动态 HMAC 密钥方案
- 解决静态密钥被逆向破解的问题
- 支持密钥定期轮换,增强安全性
游铛铛是一款BUFF选品和Steam扫货工具,通过客户端exe形式提供给用户使用。
游铛铛客户端exe项目
│
├── client-tauri/ # 🎨 前端工程 (Vue 3 + Tauri)
│ ├── src-tauri/ # Rust后端 (Tauri原生命令)
│ │ ├── src/ # Rust源代码
│ │ │ ├── commands/ # Tauri命令实现
│ │ │ │ ├── crypto/ # 加解密命令
│ │ │ │ │ ├── mod.rs
│ │ │ │ │ ├── aes.rs # AES加密/解密
│ │ │ │ │ └── rsa.rs # RSA加密
│ │ │ │ ├── auth/ # 认证命令
│ │ │ │ │ ├── mod.rs
│ │ │ │ │ ├── login.rs # 安全登录
│ │ │ │ │ ├── register.rs # 安全注册
│ │ │ │ │ └── unbind.rs # 机器解绑
│ │ │ │ ├── heartbeat.rs # 心跳检测
│ │ │ │ ├── machine.rs # 机器信息获取
│ │ │ │ └── mod.rs # 模块导出
│ │ │ ├── security.rs # 安全检查
│ │ │ ├── machine.rs # 机器码生成
│ │ │ └── main.rs # 程序入口
│ │ └── Cargo.toml # Rust依赖配置
│ ├── src/ # Vue 3 前端代码
│ │ ├── components/ # Vue组件
│ │ ├── views/ # 页面视图
│ │ ├── stores/ # Pinia状态管理
│ │ └── ...
│ └── ...
│
├── backend-client/ # ⚙️ 后端服务 (FastAPI)
│ ├── app/ # 应用代码
│ │ ├── api/ # API路由
│ │ │ ├── v1/ # API版本
│ │ │ │ ├── keys.py # 密钥相关
│ │ │ │ ├── auth.py # 认证相关
│ │ │ │ ├── announcements.py # 公告相关
│ │ │ │ ├── heartbeat.py # 心跳相关
│ │ │ │ ├── user.py # 用户相关
│ │ │ │ └── steam.py # Steam相关
│ │ │ └── deps.py # 依赖注入
│ │ ├── core/ # 核心模块
│ │ │ ├── config.py # 配置管理
│ │ │ ├── encrypt.py # 加解密工具
│ │ │ └── security.py # 安全中间件
│ │ ├── models/ # 数据模型
│ │ ├── services/ # 业务逻辑
│ │ ├── middleware/ # 中间件
│ │ └── utils/ # 工具函数
│ ├── migrations/ # 数据库迁移
│ └── requirements.txt # 依赖管理
│
├── steam-client/ # 🎮 Steam服务 (独立工程)
│ ├── steam_login/ # Steam登录
│ ├── order_confirm/ # 订单确认
│ ├── captcha_handler/ # 验证码处理
│ └── ... # 其他Steam相关
│
└── scheduled-tasks/ # ⏰ 定时任务服务 (独立工程)
├── heartbeat_check/ # 心跳检测任务
├── session_clean/ # 会话清理任务
└── ... # 其他定时任务
说明:当前仓库中 未包含 scheduled-tasks/ 独立工程目录。原因可以是以下之一或组合:
scheduled-tasks 作为独立服务尚未立项或未在本仓库中创建。app/services/heartbeat_monitor.py、app/services/session_service.py)或 backend-client 在应用内以定时/后台任务方式承担;若未做多实例部署,则暂不需要单独拆出定时任务服务。scheduled-tasks 独立进程,可避免多实例重复执行定时任务(见 1.3 划分目的)。┌─────────────────────────────────────────────────────────────────────────────┐
│ 系统整体架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ backend-admin │ │ backend-client │ │
│ │ (管理端) │ │ (客户端API) │ │
│ │ │ │ │ │
│ │ • 管理员认证 │ │ • 用户认证 │ │
│ │ • 密钥生成轮换 │ ───┬──>│ • 使用RSA公钥 │ │
│ │ • 系统配置管理 │ │ │ • 配置从DB获取 │ │
│ │ • API密钥管理 │ │ │ │ │
│ └────────┬────────┘ │ └────────┬────────┘ │
│ │ │ │ │
│ │ RSA公钥 │ │ │
│ └─────────────┘ │ │
│ ┌─────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ api_keys 表 ││
│ │ • public_key: RSA公钥 (客户端使用) ││
│ │ • private_key: RSA私钥 (仅管理端可见,客户端不可访问) ││
│ │ • key_version: 密钥版本 ││
│ │ • is_active: 是否激活 ││
│ │ • expire_at: 过期时间 ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
└─────────────────────────────────────────────────────────────────────────────┘
关键说明:
backend-admin管理端完全控制生成、轮换、过期和历史记录GET /api/keys/public接口获取最新的RSA公钥┌─────────────────────────────────────────────────────────────────────────────┐
│ 用户与认证模块 │
├─────────────────────────────────────────────────────────────────────────────┤
│ users (用户表) │
│ ├── id: 主键 │
│ ├── username: 用户名 (唯一) │
│ ├── password: 密码 (bcrypt加密) │
│ ├── machine_code: 机器码 (MD5, 绑定后不可更改) │
│ ├── card_id: 当前卡密ID │
│ ├── expire_time: 到期时间 │
│ └── status: 状态 (1正常/0禁用) │
│ │
│ sessions (会话表) │
│ ├── session_id: 会话ID (唯一) │
│ ├── user_id: 用户ID │
│ ├── machine_code: 机器码 │
│ ├── is_active: 是否激活 │
│ └── expires_at: 过期时间 │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 商业化模块 │
├─────────────────────────────────────────────────────────────────────────────┤
│ software (软件产品表) │
│ ├── code: 软件特征码 (如: buff-xp-steam-sh) │
│ ├── name: 软件名称 │
│ ├── daily_price: 每天价格基数 │
│ └── is_active: 是否启用 │
│ │
│ cards (卡密表) │
│ ├── card_code: 卡密代码 (唯一) │
│ ├── software_code: 所属软件 │
│ ├── days: 天数 │
│ ├── discount: 折扣 │
│ ├── user_id: 激活用户ID │
│ ├── activated_at: 激活时间 │
│ ├── expire_time: 到期时间 │
│ └── status: 状态 (0未激活/1已激活/2已过期) │
│ │
│ recharge_records (充值记录表) │
│ ├── user_id: 用户ID │
│ ├── card_id: 卡密ID │
│ ├── software_code: 软件特征码 │
│ ├── days: 充值天数 │
│ ├── expire_time_before/after: 充值前后到期时间 │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Steam账号模块 │
├─────────────────────────────────────────────────────────────────────────────┤
│ steam_accounts (Steam账号绑定表) │
│ ├── user_id: 用户ID │
│ ├── steam_username: Steam用户名 │
│ ├── steam_id: Steam ID │
│ ├── shared_secret: Steam Guard共享密钥 (加密存储) │
│ ├── identity_secret: Steam Guard身份密钥 (加密存储) │
│ └── is_active: 是否启用 │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 系统配置模块 (核心) │
├─────────────────────────────────────────────────────────────────────────────┤
│ system_config (系统配置表 - 纵表设计) │
│ ├── config_key: 配置键名 │
│ ├── config_value: 配置值 │
│ ├── config_type: 类型 (int/string/bool/json) │
│ ├── app_name: 所属应用 (backend-admin/backend-client) │
│ ├── is_system: 是否系统配置 │
│ └── is_editable: 是否可编辑 │
│ │
│ 【客户端核心配置项】 │
│ • heartbeat_interval_seconds: 心跳间隔秒数 │
│ • max_heartbeat_failures: 心跳最大失败次数 │
│ • max_login_attempts: 最大登录失败次数 │
│ • login_lockout_minutes: 登录锁定分钟数 │
│ • timestamp_tolerance_seconds: 时间戳容忍秒数 │
│ • prevent_duplicate_login: 是否防止重复登录 │
│ • announcement_cache_seconds: 公告缓存秒数 │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 安全通信模块 │
├─────────────────────────────────────────────────────────────────────────────┤
│ api_keys (API密钥表 - 仅backend-admin管理) │
│ ├── public_key: RSA公钥 │
│ ├── private_key: RSA私钥 (加密存储) │
│ ├── key_version: 密钥版本 │
│ ├── is_active: 是否激活 │
│ └── expire_at: 过期时间 (创建+10天) │
│ │
│ api_nonces (防重放表) │
│ ├── nonce: 随机字符串 │
│ ├── endpoint: 接口路径 │
│ ├── session_id: 会话ID │
│ └── expires_at: 过期时间 │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ API日志模块 │
├─────────────────────────────────────────────────────────────────────────────┤
│ api_logs (API请求日志表) │
│ ├── user_id: 用户ID (可为NULL) │
│ ├── endpoint: 接口路径 │
│ ├── method: HTTP方法 │
│ ├── request_data: 加密前的原始请求参数 │
│ ├── response_data: 解密后的原始响应数据 │
│ ├── ip_address: 客户端IP │
│ ├── response_code: 响应状态码 │
│ └── execution_time: 执行时间(毫秒) │
└─────────────────────────────────────────────────────────────────────────────┘
注意:
admins管理员表api_keys表的私钥对客户端完全不可见设计原则: 每个主表都有对应的 _his 历史表
主表操作流程:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ INSERT操作 │ → │ 复制到_his表 │ → │ 插入主表 │
└─────────────┘ └─────────────┘ └─────────────┘
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ UPDATE操作 │ → │ 复制旧数据 │ → │ 更新主表 │
│ │ │ 到_his表 │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ DELETE操作 │ → │ 复制到_his表 │ → │ 标记删除 │
└─────────────┘ └─────────────┘ └─────────────┘
历史表字段:
his_created_at: 历史记录创建时间his_operation: 操作类型 (INSERT/UPDATE/DELETE)| 表名 | 索引字段 | 用途 |
|---|---|---|
| users | username | 登录查询 |
| users | machine_code | 机器码验证 |
| users | expire_time | 到期检查 |
| sessions | session_id | 会话验证 |
| sessions | user_id | 用户会话查询 |
| cards | card_code | 卡密验证 |
| cards | user_id | 用户卡密查询 |
| api_nonces | expires_at | 过期清理 |
| api_logs | user_id/created_at | 日志查询 |
┌─────────────────────────────────────────────────────────────────────────────┐
│ 客户端本地安全 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 进程唯一性检查 │
│ ├── Windows: FindWindow/互斥体检测 │
│ └── 启动时检测,已运行则退出或激活已有窗口 │
│ │
│ 2. 机器码绑定机制 │
│ ├── 客户端收集机器特征信息 (硬盘序列号、MAC地址) │
│ ├── 登录时将特征信息传递给后端 │
│ ├── 后端根据特征信息生成MD5机器码 │
│ ├── 后端比对生成的机器码与数据库存储的机器码 │
│ │ ├── 数据库machine_code=NULL → 首次登录,绑定机器码 │
│ │ ├── 机器码一致 → 同一机器,允许登录 │
│ │ └── 机器码不一致 → 非同一机器,拒绝登录 │
│ ├── 登录后所有接口在报文body中传递session_id和machine_code │
│ └── 解绑操作: 需要用户触发,解除机器码绑定 │
│ │
│ 3. 防请求伪造 (session和机器码都在报文中传递) │
│ ├── 所有请求必须包含session_id │
│ ├── 所有请求必须包含machine_code │
│ ├── 所有请求必须包含时间戳 │
│ ├── 所有请求必须包含随机nonce │
│ ├── 所有请求必须包含HMAC签名 │
│ └── 后端验证签名有效性 │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ AES+RSA混合加密流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【加密流程】 │
│ │
│ 1. 客户端生成随机AES密钥 (32字节) │
│ 2. 客户端使用AES密钥加密请求数据 │
│ 3. 客户端使用RSA公钥加密AES密钥 │
│ 4. 客户端发送: encrypted_params + encrypted_aes_key + signature │
│ │
│ 5. 后端使用RSA私钥解密AES密钥 │
│ 6. 后端使用AES密钥解密请求数据 │
│ 7. 后端处理业务逻辑 │
│ 8. 后端使用AES密钥加密响应数据 │
│ 9. 后端返回: encrypted_data │
│ │
│ 10. 客户端使用本地AES密钥解密响应数据 │
│ │
│ 【加密报文结构】 │
│ │
│ 请求报文: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "encrypted_params": "base64(AES加密请求数据)", │ │
│ │ "aes_key": "base64(RSA加密AES密钥)", │ │
│ │ "key_version": 1, │ │
│ │ "timestamp": 1704105600, │ │
│ │ "nonce": "a1b2c3d4e5f6g7h8...", │ │
│ │ "signature": "base64(HMAC-SHA256签名)" │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 响应报文: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "code": 200, │ │
│ │ "msg": "操作成功", │ │
│ │ "encrypted_data": "base64(AES加密响应数据)", │ │
│ │ "timestamp": 1704105600, │ │
│ │ "nonce": "h8g7f6e5d4c3b2a1...", │ │
│ │ "signature": "base64(HMAC-SHA256签名)" │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【签名生成算法】 │
│ │
│ sign_string = f"{timestamp}:{nonce}:{encrypted_params}:{aes_key}:{key_version}"
│ signature = HMAC-SHA256(sign_string, secret_key) │
│ │
│ 【响应签名生成算法】 │
│ │
│ 后端响应时,使用相同的算法生成响应签名: │
│ response_sign_string = f"{response_timestamp}:{response_nonce}:{encrypted_data}:{key_version}"
│ response_signature = HMAC-SHA256(response_sign_string, secret_key) │
│ │
│ 【时间戳验证】 │
│ • 容忍范围: ±5分钟 (可配置) │
│ • 防止重放攻击 │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 双向签名验证体系 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ 客户端 (Rust) │ │ 服务端 (Python) │ │
│ │ client-tauri │ │ backend-client │ │
│ └──────────┬──────────┘ └──────────┬──────────┘ │
│ │ │ │
│ │ ┌───────────────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 客户端请求签名生成 │ │
│ │ ├── sign_string = timestamp:nonce:encrypted_params:aes_key:kv │ │
│ │ ├── signature = HMAC-SHA256(sign_string, CLIENT_SECRET) │ │
│ │ └── 发送请求时携带 signature │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ 加密请求 (AES+RSA) │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 2. 服务端请求签名验证 │ │
│ │ ├── 使用相同的 secret_key 重新计算签名 │ │
│ │ ├── 对比客户端传入的 signature │ │
│ │ ├── 验证失败 → 返回 400 错误 │ │
│ │ └── 验证成功 → 继续处理请求 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ 处理业务逻辑 │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 3. 服务端响应签名生成 │ │
│ │ ├── response_sign_string = rtimestamp:rnonce:encrypted_data:kv │ │
│ │ ├── response_signature = HMAC-SHA256(response_sign_string, │ │
│ │ │ SERVER_SECRET) │ │
│ │ └── 响应报文携带 response_signature │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ 加密响应 (AES) │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 4. 客户端响应签名验证 │ │
│ │ ├── 使用相同的 secret_key 重新计算签名 │ │
│ │ ├── 对比服务端返回的 response_signature │ │
│ │ ├── 验证失败 → 拒绝处理响应,弹出安全警告 │ │
│ │ └── 验证成功 → 解密并处理业务数据 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【密钥配置】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • CLIENT_SECRET: 客户端密钥 (硬编码在Rust中) │ │
│ │ - 值: "buff-client-secret-2024" │ │
│ │ - 用途: 客户端生成请求签名,服务端验证 │ │
│ │ │ │
│ │ • SERVER_SECRET: 服务端密钥 (存储在环境变量中) │ │
│ │ - 值: 从环境变量 BUFF_API_SERVER_SECRET 获取 │ │
│ │ - 用途: 服务端生成响应签名,客户端验证 │ │
│ │ │ │
│ │ • 注意: 两个密钥必须不同,实现双向认证 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【安全优势】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 请求防篡改: 客户端签名确保请求未被中间人修改 │ │
│ │ 2. 响应防篡改: 服务端签名确保响应未被中间人篡改 │ │
│ │ 3. 双向认证: 客户端和服务端互相验证对方身份 │ │
│ │ 4. 中间人防护: 同时防范请求和响应方向的中间人攻击 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 动态 HMAC 密钥方案 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【设计背景】 │
│ 静态 HMAC 密钥 (CLIENT_SECRET) 硬编码在客户端中,存在被逆向破解的风险。 │
│ 动态密钥方案通过服务端实时下发密钥,定期轮换,有效降低密钥泄露的影响范围。 │
│ │
│ 【核心特性】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 密钥动态获取: 客户端启动时从服务端获取 HMAC 密钥 │ │
│ │ 2. 定期密钥轮换: 密钥定期自动更换 (默认每1小时) │ │
│ │ 3. 用户+机器绑定: 密钥与 user_id 和 machine_code 绑定 │ │
│ │ 4. 服务端存储: 密钥存储在 Redis,支持过期和主动失效 │ │
│ │ 5. 完整审计: 所有密钥操作记录到日志表 │ │
│ │ 6. 零持久化: 客户端密钥只在内存中,不写入磁盘 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【静态密钥 vs 动态密钥】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 对比项 │ 静态密钥方案 │ 动态密钥方案 │ │
│ │ ─────────────│────────────────────────│────────────────────────│ │
│ │ 密钥存储 │ 硬编码在客户端 │ 服务端 Redis 存储 │ │
│ │ 密钥更换 │ 永不变 │ 自动定期轮换 │ │
│ │ 泄露影响 │ 全局泄露 │ 影响范围有限 │ │
│ │ 防逆向 │ 易被找到 │ 客户端无持久化密钥 │ │
│ │ 实现复杂度 │ 低 │ 中 │ │
│ │ 服务端压力 │ 低 │ 略高 │ │
│ │ 首次请求延迟 │ 无 │ 增加 1 RTT │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【密钥绑定规则】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • 绑定 user_id: 确定密钥归属的用户 │ │
│ │ • 绑定 machine_code: 确定密钥归属的设备 │ │
│ │ • 不绑定 IP 地址: 允许用户在同设备的不同网络环境下使用 │ │
│ │ • 密钥与用户+设备组合唯一 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【完整数据流转】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. 客户端首次启动 │ │
│ │ ├── 检查本地是否有缓存的有效密钥 │ │
│ │ ├── 无有效密钥 → 向服务端请求动态密钥 │ │
│ │ └── 有有效密钥 → 直接使用 │ │
│ │ │ │
│ │ 2. 请求动态密钥 (RSA 加密) │ │
│ │ ├── POST /api/keys/dynamic │ │
│ │ ├── 使用 RSA 公钥加密 AES 密钥 │ │
│ │ ├── 请求体: { machine_code, timestamp } │ │
│ │ └── 服务端验证机器码后生成动态密钥 │ │
│ │ │ │
│ │ 3. 服务端处理 │ │
│ │ ├── 验证机器码是否有效 │ │
│ │ ├── 生成随机动态密钥 (32字节) │ │
│ │ ├── 存储到 Redis (key: hmac_key:{user_id}:{machine_code}) │ │
│ │ ├── 设置过期时间 (默认1小时) │ │
│ │ └── RSA 加密后返回给客户端 │ │
│ │ │ │
│ │ 4. 客户端处理响应 │ │
│ │ ├── 解密获取动态密钥 │ │
│ │ ├── 存储到内存 (不持久化) │ │
│ │ ├── 启动刷新定时器 (默认30分钟后刷新) │ │
│ │ └── 使用动态密钥进行 HMAC 签名 │ │
│ │ │ │
│ │ 5. 密钥自动刷新 │ │
│ │ ├── 定时器触发 (每30分钟) │ │
│ │ ├── POST /api/keys/refresh │ │
│ │ ├── 使用当前动态密钥签名 │ │
│ │ ├── 服务端生成新密钥并更新 Redis │ │
│ │ └── 客户端替换使用新密钥 │ │
│ │ │ │
│ │ 6. 心跳同步更新 (可选) │ │
│ │ ├── 心跳响应中包含新密钥信息 │ │
│ │ ├── 减少额外网络请求 │ │
│ │ └── 客户端解析响应更新密钥 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【Redis 存储设计】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Key: hmac_key:{user_id}:{machine_code} │ │
│ │ Value: { │ │
│ │ "key": "abc123...", // 动态 HMAC 密钥 │ │
│ │ "key_id": "uuid-12345", // 密钥标识符 │ │
│ │ "created_at": 1704105600, // 创建时间 │ │
│ │ "expires_at": 1704109200, // 过期时间 │ │
│ │ "refresh_count": 5, // 刷新次数 │ │
│ │ "last_used_at": 1704108000 // 最后使用时间 │ │
│ │ } │ │
│ │ TTL: 3600 // 1小时过期 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【密钥分配日志表】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ hmac_key_logs (HMAC密钥日志表) │ │
│ │ ├── id: 主键 │ │
│ │ ├── user_id: 用户ID │ │
│ │ ├── machine_code: 机器码 │ │
│ │ ├── key_id: 密钥标识符 │ │
│ │ ├── action: 操作类型 (ISSUE/REFRESH/EXPIRE/REVOKE) │ │
│ │ ├── client_version: 客户端版本 │ │
│ │ ├── created_at: 创建时间 │ │
│ │ └── expires_at: 密钥过期时间 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【接口设计】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 🔑 POST /api/keys/dynamic │ │
│ │ │ 获取动态 HMAC 密钥 (未登录状态,RSA+AES加密) │ │
│ │ │ Request: { machine_code, timestamp } │ │
│ │ │ Response: { encrypted_key, key_id, expires_in } │ │
│ │ │ │ │
│ │ 🔑 POST /api/keys/refresh │ │
│ │ │ 刷新动态 HMAC 密钥 (已登录状态,动态密钥签名) │ │
│ │ │ Headers: { X-Dynamic-Key-Id: key_uuid } │ │
│ │ │ Request: { machine_code } │ │
│ │ │ Response: { encrypted_key, key_id, expires_in } │ │
│ │ │ │ │
│ │ 🔑 POST /api/heartbeat (心跳同步密钥) │ │
│ │ │ 心跳响应中可包含新密钥信息 (减少额外请求) │ │
│ │ │ Response: { ..., new_key_info: { encrypted_key, key_id } } │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【安全性增强】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 密钥与用户+机器绑定 │ │
│ │ ├── 请求时验证 user_id 和 machine_code 匹配 │ │
│ │ ├── 不匹配 → 记录安全日志,可能封禁账号 │ │
│ │ └── 防止密钥被其他用户/设备使用 │ │
│ │ │ │
│ │ 2. 异常检测 │ │
│ │ ├── 高频密钥请求检测 (5分钟内 > 3次) │ │
│ │ ├── 同一 IP 多个用户请求检测 (1小时内 > 5个) │ │
│ │ ├── 机器码变更检测 │ │
│ │ └── 检测到异常 → 记录日志 + 可能触发验证码 │ │
│ │ │ │
│ │ 3. 密钥过期机制 │ │
│ │ ├── 默认1小时过期 │ │
│ │ ├── 过期后自动失效 │ │
│ │ ├── 客户端需重新获取 │ │
│ │ └── 减少泄露密钥的长期影响 │ │
│ │ │ │
│ │ 4. 主动失效机制 │ │
│ │ ├── 支持服务端主动使密钥失效 │ │
│ │ ├── 用于处理安全事件 │ │
│ │ └── 删除 Redis 记录即可 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【代码示例】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 后端 - 获取动态密钥接口: │ │
│ │ ```python │ │
│ │ @router.post("/api/keys/dynamic") │ │
│ │ async def get_dynamic_key(request: Request): │ │
│ │ # 1. 解密请求参数 │ │
│ │ decrypted = decrypt_request(await request.body()) │ │
│ │ machine_code = decrypted.get('machine_code') │ │
│ │ timestamp = decrypted.get('timestamp') │ │
│ │ │ │
│ │ # 2. 验证时间戳 │ │
│ │ if not validate_timestamp(timestamp): │ │
│ │ raise HTTPException(status_code=400, detail="请求已过期") │ │
│ │ │ │
│ │ # 3. 验证机器码 (检查用户是否已绑定机器) │ │
│ │ user = db.query(User).filter( │ │
│ │ User.machine_code == machine_code, │ │
│ │ User.status == 1 │ │
│ │ ).first() │ │
│ │ if not user: │ │
│ │ # 新用户或未绑定机器,允许获取密钥(后续登录时会绑定) │ │
│ │ user_id = None │ │
│ │ else: │ │
│ │ user_id = user.id │ │
│ │ │ │
│ │ # 4. 生成随机动态密钥 (32字节 = 256位) │ │
│ │ import secrets │ │
│ │ dynamic_key = secrets.token_hex(32) │ │
│ │ key_id = str(uuid.uuid4()) │ │
│ │ │ │
│ │ # 5. 构建 Redis 存储结构 │ │
│ │ key_data = { │ │
│ │ "key": dynamic_key, │ │
│ │ "key_id": key_id, │ │
│ │ "created_at": time.time(), │ │
│ │ "expires_at": time.time() + 3600, # 1小时后过期 │ │
│ │ "refresh_count": 0, │ │
│ │ "last_used_at": time.time(), │ │
│ │ "user_id": user_id, │ │
│ │ "machine_code": machine_code │ │
│ │ } │ │
│ │ │ │
│ │ # 6. 存储到 Redis │ │
│ │ redis_key = f"hmac_key:{user_id or 'anonymous'}:{machine_code}"│ │
│ │ redis.setex(redis_key, 3600, json.dumps(key_data)) │ │
│ │ │ │
│ │ # 7. 记录日志 │ │
│ │ log_key_assignment(user_id, machine_code, key_id, "ISSUE") │ │
│ │ │ │
│ │ # 8. 使用 RSA 公钥加密动态密钥返回 │ │
│ │ public_key = get_rsa_public_key() │ │
│ │ encrypted_key = rsa_encrypt(dynamic_key, public_key) │ │
│ │ │ │
│ │ return { │ │
│ │ "code": 200, │ │
│ │ "encrypted_key": base64.b64encode(encrypted_key), │ │
│ │ "key_id": key_id, │ │
│ │ "expires_in": 3600, │ │
│ │ "refresh_interval": 1800 # 30分钟后刷新 │ │
│ │ } │ │
│ │ ``` │ │
│ │ │ │
│ │ 后端 - 刷新动态密钥接口: │ │
│ │ ```python │ │
│ │ @router.post("/api/keys/refresh") │ │
│ │ async def refresh_key(request: Request, │ │
│ │ key_id: str = Header(None)): │ │
│ │ # 1. 获取请求参数 │ │
│ │ decrypted = decrypt_request(await request.body()) │ │
│ │ machine_code = decrypted.get('machine_code') │ │
│ │ │ │
│ │ # 2. 获取当前密钥 │ │
│ │ redis_key = f"hmac_key:*:{machine_code}" # 查找匹配的key │ │
│ │ current_key_data = redis.get(redis_key) │ │
│ │ if not current_key_data: │ │
│ │ # 密钥不存在,需要重新获取 │ │
│ │ return {"code": 403, "msg": "密钥已过期,请重新获取"} │ │
│ │ current_data = json.loads(current_key_data) │ │
│ │ │ │
│ │ # 3. 验证 key_id 匹配 │ │
│ │ if key_id and current_data.get("key_id") != key_id: │ │
│ │ log_security_event("key_id_mismatch", machine_code) │ │
│ │ return {"code": 403, "msg": "密钥验证失败"} │ │
│ │ │ │
│ │ # 4. 验证 HMAC 签名 (使用当前动态密钥) │ │
│ │ provided_signature = decrypted.get('signature') │ │
│ │ sign_string = build_sign_string(decrypted) │ │
│ │ if not verify_hmac(sign_string, provided_signature, │ │
│ │ current_data["key"]): │ │
│ │ log_security_event("signature_verify_failed", │ │
│ │ machine_code) │ │
│ │ return {"code": 400, "msg": "签名验证失败"} │ │
│ │ │ │
│ │ # 5. 生成新密钥 │ │
│ │ new_key = secrets.token_hex(32) │ │
│ │ new_key_id = str(uuid.uuid4()) │ │
│ │ │ │
│ │ # 6. 更新 Redis │ │
│ │ current_data["key"] = new_key │ │
│ │ current_data["key_id"] = new_key_id │ │
│ │ current_data["refresh_count"] += 1 │ │
│ │ current_data["last_used_at"] = time.time() │ │
│ │ redis.setex(redis_key, 3600, json.dumps(current_data)) │ │
│ │ │ │
│ │ # 7. 记录日志 │ │
│ │ log_key_assignment(current_data.get("user_id"), │ │
│ │ machine_code, new_key_id, "REFRESH") │ │
│ │ │ │
│ │ # 8. 返回新密钥 │ │
│ │ encrypted_new_key = rsa_encrypt(new_key, get_rsa_public_key()) │ │
│ │ return { │ │
│ │ "code": 200, │ │
│ │ "encrypted_key": base64.b64encode(encrypted_new_key), │ │
│ │ "key_id": new_key_id, │ │
│ │ "expires_in": 3600 │ │
│ │ } │ │
│ │ ``` │ │
│ │ │ │
│ │ 客户端 - 获取动态密钥 (Rust): │ │
│ │ ```rust │ │
│ │ #[tauri::command] │ │
│ │ async fn get_dynamic_hmac_key() -> Result<DynamicKeyResponse, String> {
│ │ // 1. 检查本地是否有有效密钥 │ │
│ │ if let Some(cached) = get_cached_key() { │ │
│ │ if cached.expires_at > current_time() { │ │
│ │ return Ok(cached); // 使用缓存密钥 │ │
│ │ } │ │
│ │ } │ │
│ │ │ │
│ │ // 2. 获取机器码 │ │
│ │ let machine_code = get_machine_code().await?; │ │
│ │ │ │
│ │ // 3. 构建请求参数 │ │
│ │ let request_data = json!({ │ │
│ │ "machine_code": machine_code, │ │
│ │ "timestamp": get_current_timestamp() │ │
│ │ }); │ │
│ │ │ │
│ │ // 4. 使用 RSA 加密请求 │ │
│ │ let (encrypted_params, aes_key) = encrypt_with_rsa(&request_data).await?;
│ │ │ │
│ │ // 5. 发送请求 │ │
│ │ let response = api_client.post("/api/keys/dynamic") │ │
│ │ .json(&build_encrypted_request(encrypted_params, aes_key)) │ │
│ │ .send().await?; │ │
│ │ │ │
│ │ // 6. 解密响应 │ │
│ │ let encrypted_key = response.get("encrypted_key") │ │
│ │ .and_then(|v| v.as_str()) │ │
│ │ .ok_or("No encrypted key")?; │ │
│ │ │ │
│ │ let dynamic_key = rsa_decrypt(encrypted_key)?; │ │
│ │ │ │
│ │ // 7. 缓存到内存 │ │
│ │ let key_data = DynamicKeyData { │ │
│ │ key: dynamic_key.clone(), │ │
│ │ key_id: response.get("key_id") │ │
│ │ .and_then(|v| v.as_str()) │ │
│ │ .unwrap_or("").to_string(), │ │
│ │ expires_at: current_time() + response.get("expires_in") │ │
│ │ .and_then(|v| v.as_i64()) │ │
│ │ .unwrap_or(3600), │ │
│ │ refresh_interval: response.get("refresh_interval") │ │
│ │ .and_then(|v| v.as_i64()) │ │
│ │ .unwrap_or(1800) │ │
│ │ }; │ │
│ │ cache_key_to_memory(key_data); │ │
│ │ │ │
│ │ // 8. 启动刷新定时器 │ │
│ │ start_refresh_timer(key_data.refresh_interval); │ │
│ │ │ │
│ │ Ok(DynamicKeyResponse { │ │
│ │ key: dynamic_key, │ │
│ │ key_id: key_data.key_id │ │
│ │ }) │ │
│ │ } │ │
│ │ ``` │ │
│ │ │ │
│ │ 客户端 - 使用动态密钥签名 (Rust): │ │
│ │ ```rust │ │
│ │ fn sign_with_dynamic_key(data: &str, dynamic_key: &str) -> String {│ │
│ │ let mut mac = HmacSha256::new_from_slice(dynamic_key.as_bytes())│ │
│ │ .expect("HMAC init failed"); │ │
│ │ │ │
│ │ mac.update(data.as_bytes()); │ │
│ │ let signature = mac.finalize().into_bytes(); │ │
│ │ │ │
│ │ general_purpose::STANDARD.encode(signature) │ │
│ │ } │ │
│ │ ``` │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【方案选择建议】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 场景 │ 推荐方案 │ │
│ │ ─────────────────────────────────│───────────────────────────────│ │
│ │ 普通工具软件 │ 静态密钥 + 服务端防护 │ │
│ │ 高价值应用 (如游戏、支付) │ 动态密钥 │ │
│ │ 极高安全要求 (如金融、政务) │ 动态密钥 + 设备绑定 + 二次验证 │ │
│ │ 游铛铛客户端exe │ 动态密钥 (推荐) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【方案对比总结】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ✅ 动态密钥相比静态密钥的优势: │ │
│ │ 1. 即使泄露,影响范围有限 (仅当前密钥) │ │
│ │ 2. 定期轮换,攻击窗口小 │ │
│ │ 3. 完整审计日志,可追溯 │ │
│ │ 4. 支持主动失效,应对安全事件 │ │
│ │ 5. 增加破解成本 (需要持续获取新密钥) │ │
│ │ │ │
│ │ ⚠️ 需要考虑的代价: │ │
│ │ 1. 实现复杂度增加 │ │
│ │ 2. 首次请求增加网络延迟 │ │
│ │ 3. 服务端 Redis 存储和查询压力 │ │
│ │ 4. 密钥刷新失败的处理逻辑 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 服务端安全处理 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【请求签名验证】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ def verify_client_signature(params: dict, provided_signature: str) │ │
│ │ # 1. 构建签名字符串 │ │
│ │ sign_string = f"{timestamp}:{nonce}:{encrypted_params}:" │ │
│ │ f"{aes_key}:{key_version}" │ │
│ │ │ │
│ │ # 2. 使用客户端密钥计算签名 │ │
│ │ expected = hmac_sha256(sign_string, CLIENT_SECRET) │ │
│ │ │ │
│ │ # 3. 常数时间比较,防止时序攻击 │ │
│ │ return hmac.compare_digest(signature, expected) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【响应签名生成】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ def generate_server_signature(encrypted_data: str, │ │
│ │ key_version: int) -> str: │ │
│ │ # 1. 生成响应时间戳和nonce │ │
│ │ response_timestamp = get_current_timestamp() │ │
│ │ response_nonce = generate_random_nonce() │ │
│ │ │ │
│ │ # 2. 构建响应签名字符串 │ │
│ │ sign_string = f"{response_timestamp}:{response_nonce}:" │ │
│ │ f"{encrypted_data}:{key_version}" │ │
│ │ │ │
│ │ # 3. 使用服务端密钥生成签名 │ │
│ │ signature = hmac_sha256(sign_string, SERVER_SECRET) │ │
│ │ │ │
│ │ return signature, response_timestamp, response_nonce │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 1. 签名验证 │
│ ├── 验证客户端HMAC-SHA256签名 │
│ ├── 签名不匹配 → 返回400 │
│ └── 使用常数时间比较防止时序攻击 │
│ │
│ 2. 响应签名 │
│ ├── 服务端生成响应签名并返回 │
│ ├── 签名使用独立的 SERVER_SECRET │
│ └── 客户端使用相同密钥验证 │
│ │
│ 3. 时间戳验证 │
│ ├── 验证请求时间戳在容忍范围内 │
│ ├── 超时 → 返回400 │
│ └── 响应时间戳也返回给客户端 │
│ │
│ 4. Nonce防重放 │
│ ├── 验证nonce是否已使用 │
│ ├── 已使用 → 返回400 │
│ └── 自动清理过期nonce │
│ │
│ 5. 登录验证 (仅私有接口) │
│ ├── 验证session_id有效性 │
│ ├── 验证session过期时间 │
│ ├── 验证机器码匹配 │
│ ├── 验证卡密有效期 │
│ └── 验证失败 → 自动退出登录 │
│ │
│ 6. 暴力破解防护 │
│ ├── 登录失败次数限制 (从system_config获取) │
│ ├── 超过限制 → 锁定账号 │
│ └── 无验证码要求 │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 所有登录后API接口通用验证 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【设计原则】 │
│ 所有需要登录的API接口 (/api/user/*, /api/steam/*, /api/heartbeat等) │
│ 都需要进行以下4项验证,任一验证失败都会触发自动退出登录 │
│ │
│ 【验证流程】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. 验证session_id有效性 │ │
│ │ ├── 检查session_id是否存在 │ │
│ │ ├── session不存在 → 返回401,客户端自动退出登录 │ │
│ │ └── session存在 → 继续验证 │ │
│ │ │ │
│ │ 2. 验证session过期时间 │
│ │ ├── 检查session.expires_at > 当前时间 │ │
│ │ ├── 已过期 → 返回401,客户端自动退出登录 │ │
│ │ └── 未过期 → 继续验证 │ │
│ │ │ │
│ │ 3. 验证机器码匹配 │
│ │ ├── 获取请求中的machine_code │ │
│ │ ├── 比对session关联用户的machine_code │ │
│ │ ├── 不匹配 → 返回403,客户端自动退出登录 │ │
│ │ └── 匹配 → 继续验证 │ │
│ │ │ │
│ │ 4. 验证卡密有效期 │
│ │ ├── 检查用户expire_time > 当前时间 │ │
│ │ ├── 已过期 → 返回403,客户端自动退出登录 │ │
│ │ └── 有效 → 验证通过 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【验证失败处理】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 后端返回特定错误码 │ │
│ │ ├── session无效/过期 → 返回 401 {code: 401, msg: "未登录"} │ │
│ │ ├── 机器码不匹配 → 返回 403 {code: 403, msg: "设备已变更"} │ │
│ │ └── 卡密过期 → 返回 403 {code: 403, msg: "卡密已过期"} │ │
│ │ │ │
│ │ 2. 客户端收到错误后自动处理: │ │
│ │ ├── 停止心跳定时器 │ │
│ │ ├── 清除本地会话缓存 │ │
│ │ ├── 跳转到登录页面 │ │
│ │ └── 显示友好的错误提示 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【错误响应示例】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ // Session过期或无效 │ │
│ │ { │ │
│ │ "code": 401, │ │
│ │ "msg": "会话已过期,请重新登录" │ │
│ │ } │ │
│ │ │ │
│ │ // 机器码不匹配 │ │
│ │ { │ │
│ │ "code": 403, │ │
│ │ "msg": "账号已在其他设备登录,如非本人操作请联系客服" │ │
│ │ } │ │
│ │ │ │
│ │ // 卡密过期 │ │
│ │ { │ │
│ │ "code": 403, │ │
│ │ "msg": "您的卡密已到期,请前往续费或联系客户支持" │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Session自动续期流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【设计原理】 │
│ 用户活跃使用时,session会自动续期,避免用户频繁重新登录 │
│ │
│ 【续期触发条件】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 每次心跳请求时,检查session剩余时间: │ │
│ │ │ │
│ │ if (session.expires_at - current_time) < session_renewal_threshold: │ │
│ │ # 剩余时间 < 续期阈值(默认30分钟) │ │
│ │ session.expires_at = current_time + session_extension_duration │ │
│ │ # 延长session有效期 (默认延长1小时) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【配置项】 (system_config表) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 配置键 │ 默认值 │ 说明 │ │
│ │ --------------------------------│----------│-------------------│ │
│ │ session_renewal_threshold │ 1800 │ 续期阈值(秒)=30分钟 │ │
│ │ session_extension_duration │ 3600 │ 延长时间(秒)=1小时 │ │
│ │ session_default_expiry_seconds │ 86400 │ 默认有效期(24小时) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【续期流程图】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 客户端发起心跳请求 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ 获取session剩余时间: │ │ │
│ │ │ remaining = expires_at -│ │ │
│ │ │ current_time │ │ │
│ │ └───────────┬─────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ remaining < 30分钟? (1800秒) │ │ │
│ │ └──────────────────┬──────────────────────┘ │ │
│ │ │ │ │
│ │ ┌───────────┴───────────┐ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────┐ ┌─────────────────────┐ │ │
│ │ │ 是 │ │ 否 │ │ │
│ │ │ 延长session │ │ 不操作 │ │ │
│ │ └──────┬──────┘ └─────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ session.expires_at = now + 1小时 │ │ │
│ │ │ 生成历史记录 (UPDATE操作) │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【续期历史记录】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ session续期操作会自动记录到sessions_his历史表: │ │
│ │ │ │
│ │ his_created_at: NOW() │ │
│ │ his_operation: 'RENEW' (新增操作类型) │ │
│ │ expires_at_before: 续期前的过期时间 │ │
│ │ expires_at_after: 续期后的过期时间 │ │
│ │ reason: 'session_auto_renewal' │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【心跳响应返回续期状态】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "code": 200, │ │
│ │ "msg": "成功", │ │
│ │ "data": { │ │
│ │ "config": {...}, │ │
│ │ "user_info": { │ │
│ │ "expire_time": "2026-03-01", │ │
│ │ "days_remaining": 30 │ │
│ │ }, │ │
│ │ "session_info": { │ │
│ │ "expires_at": "2026-02-02T10:30:00", // 续期后的过期时间 │ │
│ │ "remaining_seconds": 3600, // 剩余秒数 │ │
│ │ "renewed": true // 是否进行了续期 │ │
│ │ } │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【客户端处理续期状态】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Rust后端解析心跳响应: │ │
│ │ │ │
│ │ if response.session_info.renewed { │ │
│ │ // Session进行了续期,更新本地session缓存 │ │
│ │ updateLocalSessionCache(response.session_info); │ │
│ │ } │ │
│ │ │ │
│ │ // 无论是否续期,都更新本地session_info │ │
│ │ updateLocalSessionInfo(response.session_info); │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 客户端签名验证 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【请求签名生成】 (Rust后端) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ fn generate_request_signature(timestamp: i64, │ │
│ │ nonce: &str, │ │
│ │ encrypted_params: &str, │ │
│ │ aes_key_base64: &str, │ │
│ │ key_version: i32) -> String: │ │
│ │ let secret_key = b"buff-client-secret-2024"; │ │
│ │ │ │
│ │ let sign_string = format!("{}:{}:{}:{}:{}", │ │
│ │ timestamp, nonce, encrypted_params, aes_key_base64, │ │
│ │ key_version); │ │
│ │ │ │
│ │ // HMAC-SHA256签名 │ │
│ │ let signature = hmac_sha256(sign_string, secret_key); │ │
│ │ base64_encode(signature) │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【响应签名验证】 (Rust后端) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ fn verify_server_signature(response: &Response, │ │
│ │ server_signature: &str) -> bool: │ │
│ │ // 1. 构建响应签名字符串 │ │
│ │ let sign_string = format!("{}:{}:{}:{}", │ │
│ │ response.timestamp, response.nonce, │ │
│ │ response.encrypted_data, response.key_version); │ │
│ │ │ │
│ │ // 2. 使用服务端密钥计算签名 │ │
│ │ let expected = hmac_sha256(sign_string, SERVER_SECRET); │ │
│ │ │ │
│ │ // 3. 常数时间比较 │ │
│ │ constant_time_eq(server_signature, &expected) │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【响应签名验证流程】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ async fn handle_api_response<T>(encrypted_response: &str) │ │
│ │ -> Result<T, ApiError>: │ │
│ │ │ │
│ │ // 1. 解析响应JSON │ │
│ │ let response: ApiResponse = parse_json(encrypted_response); │ │
│ │ │ │
│ │ // 2. 验证响应签名 │ │
│ │ let is_valid = verify_server_signature( │ │
│ │ &response, │ │
│ │ &response.signature │ │
│ │ ); │ │
│ │ │ │
│ │ if !is_valid { │ │
│ │ // 签名验证失败,可能是中间人攻击 │ │
│ │ return Err(ApiError::SignatureVerificationFailed); │ │
│ │ } │ │
│ │ │ │
│ │ // 3. 解密响应数据 │ │
│ │ let decrypted = aes_decrypt(&response.encrypted_data, │ │
│ │ &local_aes_key); │ │
│ │ │ │
│ │ // 4. 返回解密后的数据 │ │
│ │ parse_json(&decrypted) │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【签名验证失败处理】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 记录安全日志 │ │
│ │ 2. 清除本地会话缓存 │ │
│ │ 3. 跳转到登录页面 │ │
│ │ 4. 显示安全警告弹窗: │ │
│ │ "检测到响应签名验证失败,可能存在中间人攻击,\n" │ │
│ │ "为保障您的账户安全,请重新登录。" │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ RSA密钥管理架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ backend-admin (管理端) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • 密钥生成: generate_key_pair() │ │
│ │ • 密钥轮换: 自动/手动两种模式 │ │
│ │ • 密钥存储: api_keys表 (包含私钥) │ │
│ │ • 密钥过期: 默认10天 │ │
│ │ • 历史记录: 自动记录到历史表 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ GET /api/keys/public (客户端接口) │ │
│ │ 返回: { "public_key": "...", "key_version": 1 } │ │
│ │ 加密方式: AES (密钥服务内置密钥) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ backend-client (客户端API) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • 使用公钥进行RSA加密 │ │
│ │ • 验证密钥版本有效性 │ │
│ │ • 使用私钥进行RSA解密 (仅管理端可见) │ │
│ │ • 不参与密钥生成和轮换 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ client-tauri (客户端exe) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • 获取并缓存RSA公钥 │ │
│ │ • 使用公钥进行RSA加密 │ │
│ │ • RSA公钥分段混淆存储 (内存中) │ │
│ │ • 每次启动都重新获取最新公钥 │ │
│ │ • 不参与密钥生成和管理 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【客户端获取密钥流程】 │
│ │
│ 打开exe → 立即调用 /api/keys/public → 获取最新公钥 → 缓存到内存 │
│ │
│ 【密钥版本管理】 │
│ • 每次请求携带key_version │
│ • 后端验证版本有效性 │
│ • 支持多版本共存平滑过渡 │
└─────────────────────────────────────────────────────────────────────────────┘
// 分段混淆存储设计
static mut RSA_KEY_SEGMENTS: [Option<String>; 8] = [
None, None, None, None, None, None, None, None
];
// 随机存储顺序
static RSA_KEY_SEGMENT_ORDER: [usize; 8] = [3, 7, 1, 5, 0, 6, 2, 4];
// 存储流程
fn store_rsa_key_segments(public_key: String) {
// 1. Base64编码
let encoded_key = base64_encode(public_key);
// 2. 分拆为8段
let segment_size = (encoded_key.len() + 7) / 8;
// 3. 按随机顺序分散存储
for &segment_index in RSA_KEY_SEGMENT_ORDER.iter() {
let segment = extract_segment(&encoded_key, segment_size);
let noisy_segment = add_noise_marker(segment); // 添加噪声标记
RSA_KEY_SEGMENTS[segment_index] = Some(noisy_segment);
}
}
// 重组流程
fn retrieve_rsa_key_segments() -> Result<String, String> {
// 1. 按正确顺序收集所有段
// 2. 移除噪声标记
// 3. 重新组合并Base64解码
return base64_decode(combined_segments);
}
┌─────────────────────────────────────────────────────────────────────────────┐
│ 接口分类设计 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【加密方式说明】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • AES+RSA加密: 敏感数据传输,使用RSA公钥加密AES密钥 │ │
│ │ • AES加密: 仅使用服务端内置AES密钥加密 │ │
│ │ • 签名: HMAC-SHA256双向签名验证 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【核心规则】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 登录前接口 (无需session和machine_code) │ │
│ │ ├── 加密方式: AES+RSA │ │
│ │ ├── 无需验证: session、machine_code │ │
│ │ └── 需要验证: 签名、时间戳、Nonce │ │
│ │ │ │
│ │ 2. 登录后接口 (需要验证session、machine_code、卡密有效期) │ │
│ │ ├── 加密方式: AES+RSA │ │
│ │ ├── 需要验证: 签名、时间戳、Nonce、session、machine_code、卡密 │ │
│ │ └── 包括: 用户信息、Steam账号、心跳、密钥同步等 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 🔓 登录前接口 (无需登录, AES+RSA加密, 无需session验证) │
│ ├── GET /api/announcements - 获取公告列表 │
│ ├── POST /api/auth/register - 用户注册 │
│ ├── POST /api/auth/login - 用户登录 │
│ ├── POST /api/auth/recharge - 卡密充值 (需要machine_code验证) │
│ └── POST /api/auth/unbind - 机器解绑 │
│ │
│ 🔐 登录后接口 (需要登录, AES+RSA加密, 需要完整验证) │
│ ├── POST /api/user/profile - 获取用户信息 │
│ ├── POST /api/steam/bind - 绑定Steam账号 │
│ ├── POST /api/steam/unbind - 解绑Steam账号 │
│ ├── GET /api/steam/accounts - 获取已绑定的Steam账号 │
│ ├── POST /api/keys/sync - 同步RSA公钥 (AES+RSA加密) │
│ └── POST /api/heartbeat - 心跳检测 (返回system_config配置) │
│ │
│ 🔑 RSA公钥获取接口 (特殊处理) │
│ └── POST /api/keys/public - 获取RSA公钥 │
│ ├── 客户端启动时立即调用 (AES加密,服务端内置密钥) │
│ ├── 加密方式: AES (使用服务端内置AES密钥) │
│ ├── 登录前: 返回 { public_key, key_version } │
│ └── 登录后: 同样使用AES加密,但可携带session信息 │
│ │
│ 【配置获取规则】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 客户端所有配置信息必须通过心跳服务从system_config表获取: │ │
│ │ │ │
│ │ • heartbeat_interval_seconds - 心跳间隔 │ │
│ │ • max_heartbeat_failures - 心跳最大失败次数 │ │
│ │ • session_renewal_threshold - Session续期阈值 │ │
│ │ • session_extension_duration - Session延长时间 │ │
│ │ • 其他所有客户端相关配置... │ │
│ │ │ │
│ │ 客户端本地不存储任何配置,全部动态获取 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
根据客户端工程的建设思路提示词.md,整理出以下多种场景的接口设计:
【场景描述】
用户打开exe程序,系统进行初始化
【处理流程】
┌─────────────────────────────────────────────────────────────────┐
│ 客户端启动流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 单例检查 │
│ ├── FindWindow/互斥体检测 │
│ └── 已运行 → 激活窗口并退出当前 │
│ │
│ 2. 获取机器信息 │
│ ├── 硬盘序列号 │
│ ├── MAC地址 │
│ └── 生成MD5机器码 │
│ │
│ 3. 获取RSA公钥 (未登录状态,使用AES加密) │
│ ├── 生成随机AES密钥 │
│ ├── 使用AES密钥加密机器特征信息 │
│ ├── 调用: POST /api/keys/public │
│ ├── 请求: { encrypted_params, aes_key, timestamp, nonce, signature } │
│ ├── 后端使用RSA私钥解密AES密钥 │
│ ├── 后端使用AES密钥解密获取机器特征信息 │
│ ├── 后端返回: { public_key, key_version } │
│ ├── 缓存公钥到Rust内存 (分段混淆存储) │
│ └── 如果获取失败 → 显示错误提示,阻塞后续操作 │
│ │
│ 4. 获取公告列表 │
│ ├── 使用已缓存的RSA公钥 (AES+RSA加密) │
│ ├── 调用: GET /api/announcements │
│ └── 显示在登录界面 │
│ │
│ 5. 启动定时任务 │
│ ├── 密钥同步 (每60秒) │
│ │ - 仅已登录状态下执行,使用AES+RSA加密 │
│ └── 如果未登录,不启动心跳 │
│ │
. 显示登录界面 │
│ ├── 检查本地会话缓存 │
│ ├──│ 6 如果有有效会话 → 自动完成登录 │
│ └── 否则显示登录表单 │
│ │
└─────────────────────────────────────────────────────────────────┘
【场景描述】
用户在登录界面输入用户名和密码,点击登录
【处理流程】
┌─────────────────────────────────────────────────────────────────┐
│ 用户登录流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 本地验证 │
│ ├── 用户名非空 │
│ ├── 密码非空 │
│ └── 机器码已获取 │
│ │
│ 2. 获取RSA公钥 (如果内存中没有) │
│ └── 调用 GET /api/keys/public │
│ │
│ 3. 生成随机AES密钥 │
│ └── 32字节随机密钥 │
│ │
│ 4. 加密请求参数 │
│ ├── 使用AES密钥加密: {username, password, machine_code} │
│ ├── 使用RSA公钥加密AES密钥 │
│ ├── 生成时间戳和nonce │
│ └── 生成HMAC签名 │
│ │
│ 5. 发送登录请求 │
│ └── POST /api/auth/login │
│ │
│ 6. 后端处理 │
│ ├── 验证时间戳和nonce │
│ ├── 验证签名 │
│ ├── 解密请求参数 │
│ ├── 验证用户名密码 │
│ ├── 验证账户状态 │
│ ├── 验证卡密有效期 │
│ ├── 首次登录 → 生成机器码并绑定 │
│ ├── 非首次登录 → 验证机器码匹配 │
│ ├── 创建/更新会话 │
│ ├── 记录API日志 │
│ ├── 加密响应数据 (使用请求中的AES密钥) │
│ └── 返回响应 │
│ │
│ 7. 客户端处理响应 │
│ ├── 解密响应数据 │
│ ├── 登录成功 → │
│ │ ├── 保存session_id │
│ │ ├── 保存user_id │
│ │ ├── 跳转到主界面 │
│ │ └── 调用心跳服务 │
│ └── 登录失败 → 显示错误提示 │
│ │
│ 8. 登录失败处理 │
│ ├── 失败次数+1 │
│ ├── 检查是否超过限制 (从system_config获取) │
│ ├── 超过限制 → 锁定账号 │
│ └── 无验证码要求 │
│ │
└─────────────────────────────────────────────────────────────────┘
【场景描述】
用户登录成功后,立即调用心跳服务获取配置,后续定时调用
【处理流程】
┌─────────────────────────────────────────────────────────────────┐
│ 心跳检测流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 登录成功后立即调用 │
│ └── POST /api/heartbeat (AES+RSA加密) │
│ Request: { session_id, machine_code } │
│ │
│ 2. 后端处理 │
│ ├── 验证session有效性 │
│ ├── 验证机器码匹配 │
│ ├── 检查用户卡密有效期 │
│ ├── 从system_config表获取配置: │
│ │ ├── heartbeat_interval_seconds │
│ │ ├── max_heartbeat_failures │
│ │ ├── timestamp_tolerance_seconds │
│ │ └── ... │
│ ├── 获取用户状态: │
│ │ ├── expire_time │
│ │ └── days_remaining │
│ ├── 记录API日志 │
│ └── 加密响应数据 │
│ │
│ 3. 客户端处理响应 │
│ ├── 解密响应数据 │
│ ├── 更新配置缓存 │
│ ├── 更新用户状态 │
│ ├── 重置失败计数 │
│ └── 设置定时器 (根据新间隔) │
│ │
│ 4. 定时心跳调用 │
│ ├── 按配置的间隔时间定时调用 │
│ ├── 每次心跳重置失败计数 │
│ └── 失败处理: │
│ ├── 失败次数+1 │
│ ├── 检查是否超过阈值 │
│ ├── 未超过 → 顶部提示"心跳服务不可用" │
│ └── 超过 → 自动退出登录,弹窗提醒 │
│ │
│ 5. 心跳响应内容 │
│ { │
│ "config": { │
│ "heartbeat_interval_seconds": 10, │
│ "max_heartbeat_failures": 100, │
│ "timestamp_tolerance_seconds": 300, │
│ "prevent_duplicate_login": true │
│ }, │
│ "user_info": { │
│ "expire_time": "2026-03-01", │
│ "days_remaining": 30 │
│ } │
│ } │
│ │
└─────────────────────────────────────────────────────────────────┘
【场景描述】
用户已登录,但接口验证失败(session过期、机器码不匹配、卡密过期等)
【处理流程】
┌─────────────────────────────────────────────────────────────────┐
│ 验证失败处理流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 触发条件 │
│ ├── session过期/无效 │
│ ├── 机器码不匹配 │
│ ├── 卡密已过期 │
│ └── 账户被禁用 │
│ │
│ 2. 后端处理 │
│ ├── 验证失败 │
│ ├── 清除会话 (如果是重复登录) │
│ ├── 记录API日志 │
│ └── 返回错误响应 │
│ │
│ 3. 客户端处理 │
│ ├── 收到验证失败响应 │
│ ├── 停止心跳定时器 │
│ ├── 清除本地会话缓存 │
│ ├── 跳转到登录页面 │
│ ├── 显示错误提示 │
│ └── 停止所有子线程 │
│ │
│ 4. 用户体验 │
│ ├── 自动返回登录界面 │
│ ├── 显示友好的错误提示 │
│ └── 如卡密过期 → 提示续费 │
│ │
└─────────────────────────────────────────────────────────────────┘
【场景描述】
用户在登录界面或充值页面输入卡密进行充值
【处理流程】
┌─────────────────────────────────────────────────────────────────┐
│ 卡密充值流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 用户输入卡密 │
│ └── 提交充值请求 (AES+RSA加密) │
│ Request: { card_code, username, password, machine_code } │
│ │
│ 2. 后端处理逻辑 │
│ ├── 验证卡密格式 │
│ ├── 查找卡密记录 │
│ ├── 验证卡密状态: │
│ │ ├── status=0 (未激活) │
│ │ └── 未过期 │
│ ├── 验证软件类型匹配 │
│ ├── 激活卡密: │
│ │ ├── status=1 │
│ │ ├── user_id=当前用户 │
│ │ ├── activated_at=NOW() │
│ │ └── expire_time=NOW()+days │
│ ├── 更新用户信息: │
│ │ ├── card_id=卡密ID │
│ │ └── expire_time=卡密到期时间 │
│ ├── 创建充值记录 │
│ └── 加密响应数据 │
│ │
│ 3. 客户端处理响应 │
│ ├── 解密响应数据 │
│ ├── 充值成功 → │
│ │ ├── 更新本地用户信息 │
│ │ ├── 显示充值成功提示 │
│ │ └── 显示新的到期时间 │
│ └── 充值失败 → 显示错误提示 │
│ │
│ 4. 特殊说明 │
│ ├── 充值后立即生效,无需重新登录 │
│ ├── 充值接口不需要登录session │
│ └── 需要机器码验证 │
│ │
└─────────────────────────────────────────────────────────────────┘
【场景描述】
用户需要在新机器上登录,需要先解绑原机器
【处理流程】
┌─────────────────────────────────────────────────────────────────┐
│ 机器解绑流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 用户触发解绑 │
│ └── 点击"机器解绑"按钮 │
│ │
│ 2. 验证密码 │
│ └── 输入当前账户密码 │
│ │
│ 3. 发送解绑请求 (AES+RSA加密) │
│ └── POST /api/auth/unbind │
│ Request: { username, password, machine_code } │
│ │
│ 4. 后端处理 │
│ ├── 验证用户名密码 │
│ ├── users.machine_code = NULL │
│ ├── 清除用户所有会话 (强制下线) │
│ ├── 生成历史记录 │
│ └── 加密响应数据 │
│ │
│ 5. 客户端处理响应 │
│ ├── 解密响应数据 │
│ ├── 解绑成功 → │
│ │ ├── 清除本地会话缓存 │
│ │ ├── 跳转到登录页面 │
│ │ └── 提示"解绑成功,可在其他机器登录" │
│ └── 解绑失败 → 显示错误提示 │
│ │
│ 6. 登录策略总结 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ machine_code = NULL → 任意机器可登录 │ │
│ │ machine_code 有值 → 必须在绑定机器登录 │ │
│ │ 新机器尝试登录 → 拒绝+提示解绑 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
接口: POST /api/keys/public
加密: AES (密钥服务内置密钥)
无需登录
【请求流程】
1. 客户端生成随机AES密钥 (32字节)
2. 客户端收集机器特征信息 (硬盘序列号、MAC地址)
3. 客户端使用AES密钥加密机器特征信息
4. 客户端使用AES密钥加密空的请求体 {}
5. 客户端发送加密后的请求
【请求报文结构】
{
"encrypted_params": "base64(AES加密 {machine_info})",
"aes_key": "base64(AES密钥)", // 使用服务端内置AES密钥加密
"timestamp": 1704105600,
"nonce": "a1b2c3d4e5f6g7h8...",
"signature": "base64(HMAC-SHA256签名)"
}
【后端处理】
1. 使用服务端内置AES密钥解密获取aes_key
2. 使用aes_key解密encrypted_params获取机器特征信息
3. 生成MD5机器码 (用于日志记录,不绑定用户)
4. 返回当前激活的RSA公钥和密钥版本
【响应报文】
{
"code": 200,
"msg": "成功",
"data": {
"public_key": "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----",
"key_version": 1
}
}
异常处理:
- RSA公钥不存在: 返回错误,客户端显示"系统初始化中"
接口: POST /api/keys/sync
加密: AES+RSA (使用已缓存的公钥)
需要登录
【请求流程】
1. 生成随机AES密钥 (32字节)
2. 使用AES密钥加密 {machine_code, session_id}
3. 使用已缓存的RSA公钥加密AES密钥 (传递key_version)
4. 发送请求
【请求报文结构】
{
"encrypted_params": "base64(AES加密 {machine_code, session_id})",
"aes_key": "base64(RSA加密AES密钥)",
"key_version": 1, // 当前使用的密钥版本
"timestamp": 1704105600,
"nonce": "a1b2c3d4e5f6g7h8...",
"signature": "base64(HMAC-SHA256签名)"
}
【后端处理】
1. 使用RSA私钥解密获取aes_key
2. 使用aes_key解密encrypted_params获取machine_code和session_id
3. 验证session_id有效性
4. 验证machine_code与session关联的用户是否匹配
5. 返回当前激活的RSA公钥 (可能与请求中的key_version相同或更新)
【响应报文】
{
"code": 200,
"msg": "成功",
"data": {
"public_key": "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----",
"key_version": 2 // 可能更新
}
}
异常处理:
- session无效: 返回401,客户端自动退出登录
- machine_code不匹配: 返回403,客户端自动退出登录
接口: GET /api/announcements
加密: AES+RSA
无需登录
请求参数:
{
"page": 1,
"page_size": 10
}
响应数据:
{
"code": 200,
"msg": "成功",
"data": {
"announcements": [
{
"id": 1,
"title": "系统维护通知",
"content": "...",
"created_at": "2026-02-01T10:00:00"
}
],
"total": 5,
"page": 1,
"page_size": 10
}
}
接口: POST /api/auth/login
加密: AES+RSA
无需登录
请求参数:
{
"username": "test",
"password": "password123",
"machine_code": "MD5哈希值"
}
响应数据:
{
"code": 200,
"msg": "登录成功",
"data": {
"user_id": 1,
"username": "test",
"expire_time": "2026-03-01",
"days_remaining": 30
}
}
异常码:
- 400: 请求参数错误
- 401: 用户名或密码错误
- 403: 卡密已过期/机器码不匹配/账号被禁用
- 423: 账号被锁定
接口: POST /api/auth/register
加密: AES+RSA
无需登录
请求参数:
{
"username": "newuser",
"password": "password123",
"email": "user@example.com",
"card_code": "XXXX-XXXX-XXXX" // 可选,注册时激活卡密
}
响应数据:
{
"code": 200,
"msg": "注册成功",
"data": {
"user_id": 2,
"username": "newuser"
}
}
接口: POST /api/auth/recharge
加密: AES+RSA
无需登录
请求参数:
{
"username": "test",
"password": "password123",
"card_code": "XXXX-XXXX-XXXX"
}
响应数据:
{
"code": 200,
"msg": "充值成功",
"data": {
"expire_time": "2026-04-01",
"days_remaining": 60
}
}
接口: POST /api/heartbeat
加密: AES+RSA
需要登录
请求参数:
{
"session_id": "uuid-xxx",
"machine_code": "MD5哈希值"
}
响应数据:
{
"code": 200,
"msg": "成功",
"data": {
"config": {
"heartbeat_interval_seconds": 10,
"max_heartbeat_failures": 100,
"timestamp_tolerance_seconds": 300
},
"user_info": {
"expire_time": "2026-03-01",
"days_remaining": 30
}
}
}
异常处理:
- session过期/无效 → 自动退出登录
- 机器码不匹配 → 自动退出登录
- 卡密过期 → 自动退出登录
┌─────────────────────────────────────────────────────────────────────────────┐
│ 接口日志记录规范 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【日志记录内容】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 所有接口必须记录以下4类数据: │ │
│ │ │ │
│ │ 1. 入参密文 │ │
│ │ ├── 来源: 原始请求body │ │
│ │ ├── 格式: Base64编码的加密数据 │ │
│ │ └── 自动由中间件捕获 │ │
│ │ │ │
│ │ 2. 入参明文 │ │
│ │ ├── 来源: 解密后的请求参数 │ │
│ │ ├── 格式: JSON对象 │ │
│ │ └── 需要在业务代码中调用 log_decrypted_request() 记录 │ │
│ │ │ │
│ │ 3. 出参明文 │ │
│ │ ├── 来源: 业务响应数据 │ │
│ │ ├── 格式: JSON对象 │ │
│ │ └── 需要在业务代码中调用 log_decrypted_response() 记录 │ │
│ │ │ │
│ │ 4. 出参密文 │ │
│ │ ├── 来源: 加密后的响应数据 │ │
│ │ ├── 格式: Base64编码的加密数据 │ │
│ │ └── 自动由中间件捕获 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【日志存储方式】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 文件日志 (主要) │ │
│ │ ├── 路径: logs/api/api_YYYY-MM-DD.log │ │
│ │ ├── 格式: JSON Lines (每行一条JSON) │ │
│ │ ├── 轮转: 每天午夜自动创建新文件 │ │
│ │ └── 保留: 30天 │ │
│ │ │ │
│ │ 2. 数据库日志 (补充) │ │
│ │ ├── 表: api_logs │ │
│ │ ├── 内容: 请求和响应的密文(截断后) │ │
│ │ └── 用于快速查询和统计分析 │ │
│ │ │ │
│ │ 3. 控制台日志 (调试) │ │
│ │ ├── 格式: 简化版日志 │ │
│ │ └── 目的: 方便开发和调试 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【日志文件格式】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "timestamp": "2026-02-02 18:30:45.123", // 毫秒级时间戳 │ │
│ │ "endpoint": "/api/v1/auth/login", // 接口路径 │ │
│ │ "method": "POST", // HTTP方法 │ │
│ │ "user_id": 1, // 用户ID │ │
│ │ "client_ip": "192.168.1.100", // 客户端IP │ │
│ │ "processing_time_ms": 105, // 处理时间(ms) │ │
│ │ "status_code": 200, // HTTP状态码 │ │
│ │ "error": null, // 错误信息 │ │
│ │ "request": { // 请求数据 │ │
│ │ "encrypted": "...", // 密文 │ │
│ │ "decrypted": { // 明文(已脱敏) │ │
│ │ "username": "test", // │ │
│ │ "password": "****" // 已脱敏 │ │
│ │ } │ │
│ │ }, │ │
│ │ "response": { // 响应数据 │ │
│ │ "encrypted": "...", // 密文 │ │
│ │ "decrypted": { // 明文(已脱敏) │ │
│ │ "code": 200, // │ │
│ │ "msg": "成功" // │ │
│ │ } │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【敏感字段脱敏】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 以下字段会被自动脱敏 (仅显示首尾字符): │ │
│ │ │ │
│ │ • password → "****" │ │
│ │ • card_code → "****" │ │
│ │ • machine_code → "****" │ │
│ │ • shared_secret → "****" │ │
│ │ • identity_secret → "****" │ │
│ │ • aes_key → "****" │ │
│ │ • signature → "****" │ │
│ │ • access_token → "****" │ │
│ │ • refresh_token → "****" │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【业务代码记录明文示例】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ from app.utils.api_context import ( │ │
│ │ get_decrypted_request, # 获取解密后的请求 │ │
│ │ log_decrypted_request, # 记录入参明文 │ │
│ │ log_decrypted_response, # 记录出参明文 │ │
│ │ ) │ │
│ │ │ │
│ │ @router.post("/auth/login") │ │
│ │ async def login(request_data: LoginRequest): │ │
│ │ # 记录入参明文 │ │
│ │ decrypted_request = get_decrypted_request() │ │
│ │ if decrypted_request: │ │
│ │ log_decrypted_request(decrypted_request) │ │
│ │ else: │ │
│ │ log_decrypted_request(request_data.model_dump()) │ │
│ │ │ │
│ │ # 业务处理... │ │
│ │ │ │
│ │ # 记录出参明文 │ │
│ │ response_data = {"code": 200, "msg": "成功", "data": {...}} │ │
│ │ log_decrypted_response(response_data) │ │
│ │ │ │
│ │ return response_data │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【日志文件位置】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ backend-client/ │ │
│ │ └── logs/ │ │
│ │ └── api/ │ │
│ │ ├── api_2026-02-01.log │ │
│ │ ├── api_2026-02-02.log ← 当天日志 │ │
│ │ └── api_2026-02-03.log │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
# api_logs表记录内容
{
"user_id": 1, # 用户ID (可为NULL)
"endpoint": "/api/auth/login", # 接口路径
"method": "POST", # HTTP方法
"request_data": "...", # 加密前的原始请求参数
"response_data": "{...}", # 解密后的原始响应数据
"ip_address": "192.168.1.100", # 客户端IP
"user_agent": "...", # 用户代理
"response_code": 200, # 响应状态码
"execution_time": 45 # 执行时间(毫秒)
}
┌─────────────────────────────────────────────────────────────────┐
│ 客户端启动流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ 程序启动 │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 检查是否已有实例运行 (单例检查) │ │
│ │ FindWindow/互斥体检测 │ │
│ └──────────────────┬──────────────────────┘ │
│ │ 检测到已运行 │
│ ┌───────────┴───────────┐ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ 激活已有窗口 │ │ 退出程序 │ │
│ │ 并退出当前 │ └─────────────────┘ │
│ └─────────────┘ │
│ │
│ │ 未检测到运行 │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 获取本地机器信息 │ │
│ │ • 硬盘序列号 │ │
│ │ • MAC地址 │ │
│ │ • 生成MD5机器码 │ │
│ └──────────────────┬──────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 【关键】获取RSA公钥 │ │
│ │ • 无论是否有缓存 │ │
│ │ • 调用 GET /api/keys/public │ │
│ │ • 缓存到Rust内存 (分段混淆存储) │ │
│ │ • 获取失败 → 阻塞并提示 │ │
│ └──────────────────┬──────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 获取公告列表 (AES+RSA加密) │ │
│ │ • 显示在登录界面 │ │
│ └──────────────────┬──────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 启动定时任务 │ │
│ │ • 密钥同步 (每60秒) │ │
│ │ - 每次都调用密钥服务获取最新 │ │
│ │ - 更新本地缓存 │ │
│ │ • 如果已登录 → 启动心跳 │ │
│ │ • 如果未登录 → 不启动心跳 │ │
│ └──────────────────┬──────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 检查本地会话缓存 │ │
│ │ │ │
│ │ 有有效会话 + 卡密有效期内? │ │
│ │ ┌────────────┐ │ │
│ │ │ 是 │ 否 │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────┐ ┌─────────────────┐ │ │
│ │ │ 自动登录 │ │ 显示登录表单 │ │ │
│ │ │ 调用心跳 │ └─────────────────┘ │ │
│ │ └─────────────┘ │ │
│ └─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 用户登录流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 用户输入用户名/密码 → 点击登录 │
│ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 1. 验证输入参数 │ │
│ │ • 用户名非空 │ │
│ │ • 密码非空 │ │
│ │ • 机器码已获取 │ │
│ └──────────────────┬──────────────────────┘ │
│ │ 参数无效 │
│ ┌───────────┴───────────┐ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────────────┐ │
│ │ 显示错误提示 │ │ 继续处理 │ │
│ └─────────────┘ └─────────────────────┘ │
│ │
│ │ 参数有效 │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 2. 获取RSA公钥 (如果内存中没有) │ │
│ │ 调用 GET /api/keys/public │ │
│ └──────────────────┬──────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 3. 生成随机AES密钥 │ │
│ │ • 32字节随机密钥 │ │
│ │ • 用于加密请求和响应 │ │
│ └──────────────────┬──────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 4. 发送登录请求 (AES+RSA加密) │ │
│ │ • encrypted_params: 请求参数AES加密 │ │
│ │ • aes_key: RSA加密的AES密钥 │ │
│ │ • key_version │ │
│ │ • timestamp, nonce, signature │ │
│ └──────────────────┬──────────────────────┘ │
│ │ │
│ ┌───────────┴───────────┐ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────────┐ │
│ │ 登录成功 │ │ 登录失败 │ │
│ │ (code=200) │ │ (code=400/401/403) │ │
│ └────────┬────────┘ └─────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 5. 处理登录成功响应 │ │
│ │ • 解密响应数据 (使用本地AES密钥) │ │
│ │ • 保存session_id │ │
│ │ • 保存user_id │ │
│ │ • 保存machine_code │ │
│ │ • 跳转到主界面 │ │
│ │ • 启动心跳检测 │ │
│ └──────────────────┬──────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 6. 调用心跳服务 (AES+RSA加密) │ │
│ │ • 获取系统配置 (从system_config) │ │
│ │ • 启动定时心跳任务 │ │
│ │ • 心跳间隔: 从配置获取 │ │
│ │ • 最大失败次数: 从配置获取 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 登录失败处理: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ • 失败次数+1 │ │
│ │ • 检查是否超过 max_login_attempts (从system_config) │ │
│ │ • 超过限制 → 锁定账号 (login_lockout_minutes) │ │
│ │ • 显示具体错误提示 │ │
│ │ • 无验证码要求 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 心跳检测流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 登录成功 → 立即调用心跳服务 → 启动定时器 │
│ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 心跳请求 (AES+RSA加密) │ │
│ │ • session_id │ │
│ │ • machine_code │ │
│ │ • timestamp, nonce, signature │ │
│ └──────────────────┬──────────────────────┘ │
│ │ │
│ ┌───────────┴───────────┐ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────────┐ │
│ │ 心跳成功 │ │ 心跳失败 │ │
│ │ (code=200) │ │ (网络/验证失败) │ │
│ └────────┬────────┘ └─────────────────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────────────────┐ ┌────────────────┐│
│ │ │ 失败次数+1 │ │ 失败次数<100 ││
│ │ └─────────────────────┘ └───────┬────────┘│
│ │ │ │ │
│ │ │ ┌───────┴────────┐ │
│ │ │ ▼ ▼ │
│ │ │ ┌─────────────────┐ ┌─────────────┐
│ │ │ │ 继续尝试心跳 │ │ 超过100次 │
│ │ │ └────────┬────────┘ └────────┬─────┘
│ │ │ │ │
│ │ │ │ ▼
│ │ │ │ ┌─────────────────────────┐
│ │ │ │ │ 自动退出登录 │
│ │ │ │ │ • 停止心跳任务 │
│ │ │ │ │ • 清除会话缓存 │
│ │ │ │ │ • 跳转到登录页 │
│ │ │ │ │ • 弹窗提示 │
│ │ │ │ └─────────────────────────┘
│ │ │ │
│ │ │ │ 失败次数<100
│ │ │ │
│ │ │ ▼
│ │ │ ┌─────────────────────────┐
│ │ │ │ 顶部提示: 心跳服务不可用 │
│ │ │ │ (等心跳恢复后自动消失) │
│ │ │ └─────────────────────────┘
│ │ │
│ │ │ 心跳恢复
│ │ │ (失败次数重置为0)
│ │ │ │
│ │ │ ▼
│ │ │ ┌─────────────────────────┐
│ │ │ │ 顶部提示消失 │
│ │ │ │ 恢复正常状态 │
│ │ │ └─────────────────────────┘
│ │ │
│ └───────────────────┘
│ │
│ 心跳配置来源 (system_config表): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 配置键 │ 默认值 │ 说明 │ │
│ │------------------------------│----------│------------│ │
│ │ heartbeat_interval_seconds │ 10 │ 心跳间隔 │ │
│ │ max_heartbeat_failures │ 100 │ 最大失败数 │ │
│ │ timestamp_tolerance_seconds │ 300 │ 时间戳容忍 │ │
│ │ session_renewal_threshold │ 1800 │ 续期阈值(30分)│ │
│ │ session_extension_duration │ 3600 │ 延长时间(1小时)│ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Session续期说明: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ • 每次心跳检查session剩余时间 │ │
│ │ • 剩余时间 < 30分钟 (1800秒) 时触发续期 │ │
│ │ • 续期后session过期时间延长1小时 (3600秒) │ │
│ │ • 续期操作自动记录到sessions_his历史表 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
所有配置都存储在system_config表中,支持热更新:
-- 心跳相关配置 (app_name: backend-client)
INSERT INTO system_config (config_key, config_value, config_type, app_name, description) VALUES
('heartbeat_interval_seconds', '10', 'int', 'backend-client', '心跳检测间隔秒数'),
('max_heartbeat_failures', '100', 'int', 'backend-client', '心跳最大失败次数'),
-- Session自动续期配置
('session_renewal_threshold', '1800', 'int', 'backend-client', 'Session续期阈值(秒),剩余时间小于此值时触发续期'),
('session_extension_duration', '3600', 'int', 'backend-client', 'Session延长时间(秒),续期时延长的时长'),
('session_default_expiry_seconds', '86400', 'int', 'backend-client', 'Session默认有效期(秒),新登录时创建的有效期'),
-- 安全相关配置
('timestamp_tolerance_seconds', '300', 'int', 'backend-client', '时间戳容忍秒数(±5分钟)'),
('prevent_duplicate_login', 'true', 'bool', 'backend-client', '是否防止重复登录'),
-- 登录相关配置
('max_login_attempts', '5', 'int', 'backend-client', '最大登录失败次数'),
('login_lockout_minutes', '60', 'int', 'backend-client', '登录锁定分钟数'),
-- 公告相关配置
('announcement_cache_seconds', '300', 'int', 'backend-client', '公告缓存秒数'),
-- 密钥相关配置 (客户端只读)
('key_sync_interval_seconds', '60', 'int', 'backend-client', '密钥同步间隔秒数');
┌─────────────────────────────────────────────────────────────────┐
│ 配置获取流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 配置来源: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. 客户端启动时 │ │
│ │ - 调用心跳服务获取配置 │ │
│ │ - 首次获取完整配置 │ │
│ │ │ │
│ │ 2. 心跳响应中返回 │ │
│ │ - 每次心跳都返回最新配置 │ │
│ │ - 自动更新本地配置 │ │
│ │ │ │
│ │ 3. 配置变更后 │ │
│ │ - 管理员在backend-admin中修改配置 │ │
│ │ - 配置存储到system_config表 │ │
│ │ - 下次心跳时客户端自动获取最新配置 │ │
│ │ - 无需重启应用 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 配置变更流程: │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 管理员修改 │ → │ 更新数据库 │ → │ 心跳时同步 │ │
│ │ system_config│ │ │ │ 到客户端 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ ↓ │
│ ┌─────────────┐ │
│ │ 客户端生效 │ │
│ │ 无需重启 │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘
from typing import Any
import json
class ConfigValidator:
"""配置验证器"""
@staticmethod
def get_config(db, config_key: str, app_name: str) -> Any:
"""获取配置值"""
config = db.query(SystemConfig).filter(
SystemConfig.config_key == config_key,
SystemConfig.app_name.contains(app_name),
SystemConfig.is_deleted == 0
).first()
if not config:
return None
# 类型转换
if config.config_type == 'int':
return int(config.config_value)
elif config.config_type == 'bool':
return config.config_value.lower() == 'true'
elif config.config_type == 'json':
return json.loads(config.config_value)
else:
return config.config_value
@staticmethod
def get_heartbeat_interval(db) -> int:
"""获取心跳间隔(秒)"""
return ConfigValidator.get_config(db, 'heartbeat_interval_seconds', 'backend-client') or 10
@staticmethod
def get_max_heartbeat_failures(db) -> int:
"""获取最大心跳失败次数"""
return ConfigValidator.get_config(db, 'max_heartbeat_failures', 'backend-client') or 100
# ========== Session续期配置 ==========
@staticmethod
def get_session_renewal_threshold(db) -> int:
"""获取Session续期阈值(秒),剩余时间小于此值时触发续期"""
return ConfigValidator.get_config(db, 'session_renewal_threshold', 'backend-client') or 1800
@staticmethod
def get_session_extension_duration(db) -> int:
"""获取Session延长时间(秒)"""
return ConfigValidator.get_config(db, 'session_extension_duration', 'backend-client') or 3600
@staticmethod
def get_session_default_expiry(db) -> int:
"""获取Session默认有效期(秒)"""
return ConfigValidator.get_config(db, 'session_default_expiry_seconds', 'backend-client') or 86400
@staticmethod
def should_renew_session(db, session_expires_at) -> tuple:
"""检查是否应该续期Session
Args:
session_expires_at: session过期时间 (datetime)
Returns:
tuple: (should_renew: bool, new_expires_at: datetime or None)
"""
import datetime
current_time = datetime.datetime.utcnow()
remaining = (session_expires_at - current_time).total_seconds()
threshold = ConfigValidator.get_session_renewal_threshold(db)
if remaining < threshold:
# 需要续期
extension = ConfigValidator.get_session_extension_duration(db)
new_expires_at = current_time + datetime.timedelta(seconds=extension)
return True, new_expires_at
return False, None
┌─────────────────────────────────────────┐
│ Vue 3 + TypeScript │
│ (Composition API + <script setup>) │
└──────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Pinia (状态管理) │
│ • authStore - 认证状态 │
│ • configStore - 配置状态 │
│ • userStore - 用户信息 │
│ • heartbeatStore - 心跳状态 │
└──────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Tauri (Rust后端) │
│ • secure_api_call - 加密API调用 │
│ • key_manager - 密钥管理 │
│ • machine_info - 机器信息获取 │
│ • heartbeat - 心跳检测 │
└──────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Rust原生代码 (src-tauri) │
│ • AES加密/解密 │
│ • RSA公钥分段混淆存储 │
│ • 机器信息收集 │
│ • 进程单例检查 │
└─────────────────────────────────────────┘
src/
├── components/
│ ├── common/
│ │ ├── Loading.vue # 加载组件
│ │ ├── Message.vue # 消息提示
│ │ └── Modal.vue # 弹窗组件
│ ├── auth/
│ │ ├── LoginForm.vue # 登录表单
│ │ ├── RegisterForm.vue # 注册表单
│ │ ├── RechargeForm.vue # 充值表单
│ │ └── UnbindDialog.vue # 解绑弹窗
│ ├── announcement/
│ │ └── AnnouncementList.vue # 公告列表
│ └── layout/
│ ├── Header.vue # 顶部栏 (含心跳状态)
│ ├── Sidebar.vue # 侧边栏
│ └── MainLayout.vue # 主布局
├── views/
│ ├── LoginView.vue # 登录页面
│ ├── RegisterView.vue # 注册页面
│ ├── HomeView.vue # 首页/仪表盘
│ ├── RechargeView.vue # 充值页面
│ └── SettingsView.vue # 设置页面
├── stores/
│ ├── auth.ts # 认证状态管理
│ ├── config.ts # 配置状态管理
│ ├── user.ts # 用户信息管理
│ └── heartbeat.ts # 心跳状态管理
├── services/
│ ├── api.ts # API封装
│ ├── crypto.ts # 加解密服务
│ └── heartbeat.ts # 心跳服务
├── utils/
│ ├── machine.ts # 机器码工具
│ └── validator.ts # 表单验证
└── App.vue
src-tauri/src/
├── commands/
│ ├── mod.rs # 模块导出
│ ├── crypto/
│ │ ├── mod.rs
│ │ ├── aes.rs # AES加密/解密
│ │ └── rsa.rs # RSA加密
│ ├── key/
│ │ ├── mod.rs
│ │ ├── manager.rs # 密钥管理
│ │ └── storage.rs # 密钥分段存储
│ ├── auth/
│ │ ├── mod.rs
│ │ ├── login.rs # 安全登录
│ │ ├── register.rs # 安全注册
│ │ └── unbind.rs # 安全解绑
│ ├── heartbeat.rs # 心跳检测
│ └── machine.rs # 机器信息获取
├── security.rs # 安全检查
├── machine.rs # 机器码生成
└── main.rs # 程序入口
// 获取RSA公钥
#[tauri::command]
pub async fn get_public_key() -> Result<PublicKeyResponse, String> {
// 调用后端API获取公钥
let response = api_client.get("/api/keys/public").await?;
// 解析响应
let public_key = response.get("public_key")
.and_then(|v| v.as_str())
.ok_or("No public key in response")?;
let key_version = response.get("key_version")
.and_then(|v| v.as_i64())
.ok_or("No key version in response")? as i32;
// 分段混淆存储
store_rsa_key_segments(public_key.to_string());
Ok(PublicKeyResponse {
public_key: public_key.to_string(),
key_version
})
}
// 检查是否有缓存的公钥
#[tauri::command]
pub fn has_stored_rsa_key() -> bool {
retrieve_rsa_key_segments().is_ok()
}
┌─────────────────────────────────────────┐
│ FastAPI (Python) │
│ • 自动API文档 (Swagger/OpenAPI) │
│ • 请求验证 (Pydantic) │
│ • 依赖注入 │
└──────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ SQLAlchemy (ORM) │
│ • MySQL数据库操作 │
│ • 自动迁移 (Alembic) │
└──────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Redis (可选) │
│ • 会话缓存 │
│ • 配置缓存 │
│ • 限流计数 │
└──────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 安全中间件 │
│ • RSA+AES解密 │
│ • 签名验证 │
│ • 时间戳验证 │
│ • Nonce防重放 │
│ • 会话验证 │
│ • 机器码验证 │
│ • 卡密有效期验证 │
└─────────────────────────────────────────┘
app/
├── api/
│ ├── __init__.py
│ ├── v1/
│ │ ├── __init__.py
│ │ ├── keys.py # 密钥相关
│ │ │ └── GET /api/keys/public (AES加密)
│ │ ├── auth.py # 认证相关
│ │ │ ├── POST /api/auth/register (AES+RSA)
│ │ │ ├── POST /api/auth/login (AES+RSA)
│ │ │ ├── POST /api/auth/recharge (AES+RSA)
│ │ │ └── POST /api/auth/unbind (AES+RSA)
│ │ ├── announcements.py # 公告相关
│ │ │ └── GET /api/announcements (AES+RSA)
│ │ ├── heartbeat.py # 心跳相关
│ │ │ └── POST /api/heartbeat (AES+RSA, 需要登录)
│ │ ├── user.py # 用户相关
│ │ │ └── POST /api/user/profile (AES+RSA, 需要登录)
│ │ └── steam.py # Steam相关
│ │ ├── POST /api/steam/bind (AES+RSA, 需要登录)
│ │ ├── POST /api/steam/unbind (AES+RSA, 需要登录)
│ │ └── GET /api/steam/accounts (AES+RSA, 需要登录)
│ └── deps.py # 依赖注入
├── core/
│ ├── __init__.py
│ ├── config.py # 配置管理 (从system_config读取)
│ ├── security.py # 安全中间件
│ ├── encrypt.py # 加解密工具
│ └── database.py # 数据库连接
├── models/
│ ├── __init__.py
│ ├── user.py # 用户模型
│ ├── session.py # 会话模型
│ ├── card.py # 卡密模型
│ ├── announcement.py # 公告模型
│ ├── config.py # 配置模型
│ └── api_key.py # 密钥模型 (只读)
├── schemas/
│ ├── __init__.py
│ ├── auth.py # 认证请求/响应
│ ├── announcement.py # 公告请求/响应
│ ├── heartbeat.py # 心跳请求/响应
│ └── ...
├── services/
│ ├── __init__.py
│ ├── auth_service.py # 认证服务
│ ├── heartbeat_service.py # 心跳服务
│ ├── recharge_service.py # 充值服务
│ └── ...
├── middleware/
│ ├── __init__.py
│ ├── audit.py # API日志记录
│ └── auth.py # 认证中间件
└── utils/
├── __init__.py
├── logger.py # 日志工具
└── history.py # 历史记录工具
import os
import hmac
import hashlib
import base64
import time
import secrets
import datetime
from fastapi import Request, HTTPException
from functools import wraps
from .encrypt import decrypt_request, encrypt_response
from .services.session import verify_session, renew_session
from .services.machine import verify_machine_code
from .services.config import get_config
from .utils.history import create_history_record
# 服务端签名密钥 (从环境变量获取)
SERVER_SECRET = os.getenv("BUFF_API_SERVER_SECRET", "buff-server-secret-2024").encode()
def generate_response_signature(encrypted_data: str, key_version: int) -> tuple:
"""生成响应签名
Returns:
signature: Base64编码的HMAC-SHA256签名
timestamp: 响应时间戳
nonce: 响应随机字符串
"""
timestamp = int(time.time())
nonce = secrets.token_hex(16)
# 构建响应签名字符串
sign_string = f"{timestamp}:{nonce}:{encrypted_data}:{key_version}"
# 使用服务端密钥生成签名
signature = hmac.new(
SERVER_SECRET,
sign_string.encode(),
hashlib.sha256
).digest()
return base64.b64encode(signature).decode(), timestamp, nonce
def verify_client_signature(params: dict, provided_signature: str, secret_key: bytes) -> bool:
"""验证客户端签名 (使用常数时间比较防止时序攻击)"""
try:
timestamp = str(params.get('timestamp', ''))
nonce = params.get('nonce', '')
encrypted_params = params.get('encrypted_params', '')
aes_key = params.get('aes_key', '')
key_version = str(params.get('key_version', ''))
# 构建签名字符串
sign_string = f"{timestamp}:{nonce}:{encrypted_params}:{aes_key}:{key_version}"
# 计算期望的签名
expected_signature = hmac.new(
secret_key,
sign_string.encode(),
hashlib.sha256
).digest()
# 常数时间比较 (防止时序攻击)
provided = base64.b64decode(provided_signature)
return hmac.compare_digest(provided, expected_signature)
except Exception:
return False
def verify_session_and_user_status(db, session_id: str, machine_code: str) -> tuple:
"""验证session和用户状态 (通用验证逻辑)
用于所有登录后API接口的通用验证
Returns:
tuple: (is_valid: bool, session: Session, error_code: int, error_msg: str)
"""
# 1. 验证session_id有效性
session = verify_session(session_id)
if not session:
return False, None, 401, "会话已过期,请重新登录"
# 2. 验证session过期时间
if session.expires_at < datetime.datetime.utcnow():
return False, None, 401, "会话已过期,请重新登录"
# 3. 验证机器码匹配
if not verify_machine_code(session.user_id, machine_code):
return False, None, 403, "账号已在其他设备登录,如非本人操作请联系客服"
# 4. 验证卡密有效期
if not check_user_expire_time(session.user_id):
return False, None, 403, "您的卡密已到期,请前往续费或联系客户支持"
return True, session, None, None
def renew_session_if_needed(db, session) -> dict:
"""检查并续期session
Returns:
dict: {
"renewed": bool,
"expires_at": datetime or None,
"remaining_seconds": int
}
"""
current_time = datetime.datetime.utcnow()
remaining = (session.expires_at - current_time).total_seconds()
# 获取续期阈值配置
renewal_threshold = get_config('session_renewal_threshold', 1800) or 1800
result = {
"renewed": False,
"expires_at": session.expires_at.isoformat(),
"remaining_seconds": int(remaining)
}
if remaining < renewal_threshold:
# 需要续期
extension_duration = get_config('session_extension_duration', 3600) or 3600
new_expires_at = current_time + datetime.timedelta(seconds=extension_duration)
# 更新session
old_expires_at = session.expires_at
session.expires_at = new_expires_at
db.add(session)
db.flush()
# 创建历史记录
create_history_record(
db,
"sessions",
session,
"RENEW",
f"Session自动续期: {old_expires_at} -> {new_expires_at}"
)
db.commit()
result["renewed"] = True
result["expires_at"] = new_expires_at.isoformat()
return result
def secure_endpoint(require_login: bool = True, client_secret: bytes = None):
"""安全端点装饰器
Args:
require_login: 是否需要登录验证 (所有登录后API都应设为True)
client_secret: 客户端签名密钥 (用于验证客户端请求签名)
"""
# 默认使用环境变量中的客户端密钥
if client_secret is None:
client_secret = os.getenv("BUFF_API_CLIENT_SECRET", "buff-client-secret-2024").encode()
def decorator(func):
@wraps(func)
async def wrapper(request: Request, *args, **kwargs):
from app.database import get_db
# 1. 获取并解密请求参数
encrypted_body = await request.body()
decrypted_params = decrypt_request(encrypted_body)
# 2. 验证时间戳 (从system_config获取容忍范围)
timestamp = decrypted_params.get('timestamp', 0)
tolerance = get_config('timestamp_tolerance_seconds', 300)
current_time = int(time.time())
if abs(current_time - timestamp) > tolerance:
raise HTTPException(status_code=400, detail="请求已过期")
# 3. 验证Nonce (防重放)
nonce = decrypted_params.get('nonce')
if not verify_nonce(nonce, request.url.path):
raise HTTPException(status_code=400, detail="重复请求")
# 4. 验证客户端签名
client_signature = decrypted_params.get('signature', '')
if not verify_client_signature(decrypted_params, client_signature, client_secret):
raise HTTPException(status_code=400, detail="签名验证失败")
# 5. 通用验证 (所有登录后API接口)
session = None
if require_login:
session_id = decrypted_params.get('session_id')
machine_code = decrypted_params.get('machine_code')
# 获取数据库连接
db = get_db()
# 执行通用验证
is_valid, session, error_code, error_msg = verify_session_and_user_status(
db, session_id, machine_code
)
if not is_valid:
raise HTTPException(status_code=error_code, detail=error_msg)
# 将session注入到请求中
request.state.session = session
request.state.db = db
# 6. 执行实际业务逻辑
response_data = await func(decrypted_params, request.state, *args, **kwargs)
# 7. 心跳接口特殊处理: session续期
session_info = {}
if require_login and session:
session_info = renew_session_if_needed(db, session)
# 8. 加密响应数据
encrypted_response, aes_key = encrypt_response(response_data)
# 9. 生成响应签名
response_signature, response_timestamp, response_nonce = \
generate_response_signature(encrypted_response, decrypted_params.get('key_version', 1))
# 10. 返回加密响应和签名
result = {
"code": 200,
"msg": "成功",
"encrypted_data": encrypted_response,
"timestamp": response_timestamp,
"nonce": response_nonce,
"signature": response_signature,
"key_version": decrypted_params.get('key_version', 1)
}
# 心跳接口返回session续期信息
if session_info:
result["data"] = {
"config": response_data.get("config", {}),
"user_info": response_data.get("user_info", {}),
"session_info": session_info
}
return result
return wrapper
return decorator
from app.models.system_config import SystemConfig
from sqlalchemy.orm import Session
class ConfigService:
"""配置服务"""
@staticmethod
def get_config(db: Session, config_key: str, app_name: str = 'backend-client') -> str:
"""获取配置值"""
config = db.query(SystemConfig).filter(
SystemConfig.config_key == config_key,
SystemConfig.app_name.contains(app_name),
SystemConfig.is_deleted == 0
).first()
return config.config_value if config else None
@staticmethod
def get_heartbeat_interval(db: Session) -> int:
"""获取心跳间隔"""
value = ConfigService.get_config(db, 'heartbeat_interval_seconds')
return int(value) if value else 10
@staticmethod
def get_max_heartbeat_failures(db: Session) -> int:
"""获取最大心跳失败次数"""
value = ConfigService.get_config(db, 'max_heartbeat_failures')
return int(value) if value else 100
@staticmethod
def get_max_login_attempts(db: Session) -> int:
"""获取最大登录失败次数"""
value = ConfigService.get_config(db, 'max_login_attempts')
return int(value) if value else 5
┌─────────────────────────────────────────────────────────────────────────────┐
│ 完整数据流转图 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 前端 (Vue3+Tauri) │
│ │ │
│ │ 1. 收集机器信息 │
│ │ 2. 生成MD5机器码 │
│ │ 3. 调用Tauri命令 │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ Tauri Rust命令层 │ │
│ │ • secure_api_call (加密请求) │ │
│ │ • 获取RSA公钥 │ │
│ │ • 解密响应并返回给前端 │ │
│ └──────────────────┬──────────────────────┘ │
│ │ │
│ │ 4. 加密请求 (AES+RSA) │
│ │ - 生成随机AES密钥 │
│ │ - 使用AES密钥加密请求 │
│ │ - 使用RSA公钥加密AES密钥 │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ backend-client FastAPI服务 │ │
│ │ │ │
│ │ 中间件层: │ │
│ │ • 解密请求参数 │ │
│ │ • 验证签名 │ │
│ │ • 验证时间戳 │ │
│ │ • 验证Nonce (防重放) │ │
│ │ │ │
│ │ 认证中间件: │ │
│ │ • 验证session │ │
│ │ • 验证机器码 │ │
│ │ • 验证卡密有效期 │ │
│ │ │ │
│ │ 业务层: │ │
│ │ • 处理业务逻辑 │ │
│ │ • 操作数据库 │ │
│ │ • 从system_config获取配置 │ │
│ │ • 记录API日志 │ │
│ │ • 生成响应数据 │ │
│ │ │ │
│ │ 响应层: │ │
│ │ • 使用请求中的AES密钥加密响应 │ │
│ │ • 返回加密响应 │ │
│ └──────────────────┬──────────────────────┘ │
│ │ │
│ │ 5. 加密响应 │
│ │ - 使用请求中的AES密钥加密 │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ Tauri Rust命令层 │ │
│ │ • 解密响应数据 (使用本地AES密钥) │ │
│ │ • 返回明文给前端 │ │
│ └──────────────────┬──────────────────────┘ │
│ │ │
│ ▼ │
│ 前端更新UI │
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ 数据库层 (MySQL) │ │
│ │ │ │
│ │ 主表操作 → 自动记录到历史表 (_his) │ │
│ │ │ │
│ │ users → users_his │ │
│ │ cards → cards_his │ │
│ │ sessions → sessions_his │ │
│ │ system_config (动态配置) │ │
│ │ api_keys (RSA密钥) │ │
│ │ api_nonces (防重放) │ │
│ │ api_logs (API日志) │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 心跳检测数据流转 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 登录成功 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 调用心跳服务 (AES+RSA加密) │
│ │ POST /api/heartbeat - 心跳检测 (返回系统配置)
│ │ Request: {session_id, machine_code} │ │
│ └──────────────────┬──────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 后端处理 │ │
│ │ │ │
│ │ 1. 验证session有效性 │ │
│ │ 2. 验证机器码 │ │
│ │ 3. 检查用户卡密有效期 │ │
│ │ │ │
│ │ 4. 从system_config表获取配置: │ │
│ │ • heartbeat_interval_seconds │ │
│ │ • max_heartbeat_failures │ │
│ │ • timestamp_tolerance_seconds │ │
│ │ │ │
│ │ 5. 获取用户状态: │ │
│ │ • expire_time │ │
│ │ • days_remaining │ │
│ │ │ │
│ │ 6. 记录API日志 │ │
│ │ │ │
│ │ 7. 加密响应数据 (使用请求中的AES密钥) │ │
│ └──────────────────┬──────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 前端处理响应 │ │
│ │ │ │
│ │ 1. 解密响应数据 │ │
│ │ 2. 更新配置缓存 │ │
│ │ 3. 更新用户状态 │ │
│ │ 4. 重置失败计数 │ │
│ │ 5. 设置定时器 (根据新间隔) │ │
│ │ │ │
│ │ 失败处理: │ │
│ │ • 失败次数+1 │ │
│ │ • 更新UI提示 │ │
│ │ • 超过阈值 → 退出登录 │ │
│ └─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
| 功能 | backend-admin | backend-client | client-tauri |
|---|---|---|---|
| 管理员认证 | ✅ | ❌ | ❌ |
| RSA密钥生成 | ✅ | ❌ | ❌ |
| RSA密钥轮换 | ✅ | ❌ | ❌ |
| RSA密钥存储 | ✅ | ❌ (只读) | ❌ (只存公钥) |
| 用户认证 | ❌ | ✅ | ✅ |
| 系统配置管理 | ✅ (增删改) | ✅ (读取) | ✅ (使用) |
| 会话管理 | ❌ | ✅ | ✅ |
| API日志记录 | ❌ | ✅ | ❌ |
┌─────────────────────────────────────────────────────────────────────────────┐
│ RSA公钥获取与同步规范 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【公钥获取时机】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 客户端exe启动时: │ │
│ │ - 立即调用 POST /api/keys/public 获取RSA公钥 │ │
│ │ - 使用AES加密方式 (无需RSA公钥即可调用) │ │
│ │ - 获取失败则阻塞后续操作,显示错误提示 │ │
│ │ │ │
│ │ 2. 登录成功后定时同步: │ │
│ │ - 每60秒调用 POST /api/keys/sync │ │
│ │ - 使用AES+RSA加密方式 │ │
│ │ - 携带session_id和machine_code进行验证 │ │
│ │ - 获取最新的公钥并更新本地缓存 │ │
│ │ │ │
│ │ 3. 公钥版本变更时: │ │
│ │ - 后端返回新版本公钥时立即更新 │ │
│ │ - 下次请求使用新公钥 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【加密方式说明】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ /api/keys/public (启动时调用) │ │
│ │ ├── 加密方式: AES (使用服务端内置AES密钥) │ │
│ │ ├── 请求体: {} (空JSON) │ │
│ │ ├── 后端使用内置AES密钥解密 │ │
│ │ └── 返回: { public_key, key_version } │ │
│ │ │ │
│ │ /api/keys/sync (登录后定时调用) │ │
│ │ ├── 加密方式: AES+RSA (使用已缓存的RSA公钥) │ │
│ │ ├── 请求体: { session_id, machine_code } │ │
│ │ ├── 后端验证session和machine_code │ │
│ │ └── 返回: { public_key, key_version } │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 接口访问控制规则 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【登录前接口】 (无需session和machine_code验证) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 接口 │ 加密方式 │ 需要验证项 │ │
│ │ -----------------------│-----------│------------------------------│ │
│ │ POST /api/auth/login │ AES+RSA │ 签名、时间戳、Nonce │ │
│ │ POST /api/auth/register│ AES+RSA │ 签名、时间戳、Nonce │ │
│ │ POST /api/auth/recharge│ AES+RSA │ 签名、时间戳、Nonce、machine_code│
│ │ POST /api/auth/unbind │ AES+RSA │ 签名、时间戳、Nonce、machine_code│
│ │ GET /api/announcements │ AES+RSA │ 签名、时间戳、Nonce │ │
│ │ POST /api/keys/public │ AES │ 签名、时间戳、Nonce │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【登录后接口】 (需要session、machine_code、卡密有效期验证) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 接口 │ 加密方式 │ 需要验证项 │ │
│ │ -----------------------│-----------│------------------------------│ │
│ │ POST /api/heartbeat │ AES+RSA │ 签名、Nonce、session、 │ │
│ │ │ │ machine_code、卡密有效期 │ │
│ │ POST /api/user/profile │ AES+RSA │ 签名、Nonce、session、 │ │
│ │ │ │ machine_code、卡密有效期 │ │
│ │ POST /api/steam/bind │ AES+RSA │ 签名、Nonce、session、 │ │
│ │ │ │ machine_code、卡密有效期 │ │
│ │ POST /api/steam/unbind │ AES+RSA │ 签名、Nonce、session、 │ │
│ │ │ │ machine_code、卡密有效期 │ │
│ │ GET /api/steam/accounts│ AES+RSA │ 签名、Nonce、session、 │ │
│ │ │ │ machine_code、卡密有效期 │ │
│ │ POST /api/keys/sync │ AES+RSA │ 签名、Nonce、session、 │ │
│ │ │ │ machine_code、卡密有效期 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【验证失败处理规则】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 错误类型 │ HTTP状态码 │ 客户端处理 │ │
│ │ ---------------------│-----------│------------------------------│ │
│ │ session无效/过期 │ 401 │ 自动退出登录 │ │
│ │ 机器码不匹配 │ 403 │ 自动退出登录 │ │
│ │ 卡密已过期 │ 403 │ 自动退出登录,提示续费 │ │
│ │ 账户被禁用 │ 403 │ 自动退出登录 │ │
│ │ 心跳服务异常 │ 5xx │ 累计失败次数,超限退出 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 心跳服务设计规范 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【心跳触发时机】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 登录成功后立即调用 │ │
│ │ 2. 之后按配置间隔定时调用 │ │
│ │ 3. 间隔时间从心跳响应config中获取 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【心跳请求参数】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "session_id": "uuid-xxx", │ │
│ │ "machine_code": "MD5哈希值", │ │
│ │ "timestamp": 1704105600, │ │
│ │ "nonce": "a1b2c3d4...", │ │
│ │ "signature": "base64签名" │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【心跳后端处理】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 验证签名 │ │
│ │ 2. 验证时间戳和Nonce │ │
│ │ 3. 验证session有效性和过期时间 │ │
│ │ 4. 验证machine_code匹配 │ │
│ │ 5. 验证卡密有效期 │ │
│ │ 6. 检查session是否需要续期 │ │
│ │ 7. 从system_config获取客户端配置 │ │
│ │ 8. 获取用户状态信息 │ │
│ │ 9. 生成响应签名并返回 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【心跳响应内容】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "code": 200, │ │
│ │ "msg": "成功", │ │
│ │ "encrypted_data": "base64(AES加密数据)", │ │
│ │ "timestamp": 1704105601, │ │
│ │ "nonce": "h8g7f6...", │ │
│ │ "signature": "base64(服务端签名)", │ │
│ │ "key_version": 1 │ │
│ │ } │ │
│ │ │ │
│ │ 解密后data内容: │ │
│ │ { │ │
│ │ "config": { // 从system_config获取 │ │
│ │ "heartbeat_interval_seconds": 10, │ │
│ │ "max_heartbeat_failures": 100, │ │
│ │ "session_renewal_threshold": 1800, │ │
│ │ "session_extension_duration": 3600, │ │
│ │ "timestamp_tolerance_seconds": 300, │ │
│ │ ... 其他所有客户端配置 │ │
│ │ }, │ │
│ │ "user_info": { │ │
│ │ "expire_time": "2026-03-01", │ │
│ │ "days_remaining": 30 │ │
│ │ }, │ │
│ │ "session_info": { │ │
│ │ "expires_at": "2026-02-02T10:30:00", │ │
│ │ "remaining_seconds": 3600, │ │
│ │ "renewed": true │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【配置获取规则】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 客户端所有配置必须通过心跳服务从system_config表动态获取: │ │
│ │ │ │
│ │ - heartbeat_interval_seconds - 心跳间隔秒数 │ │
│ │ - max_heartbeat_failures - 心跳最大失败次数 │ │
│ │ - session_renewal_threshold - Session续期阈值(秒) │ │
│ │ - session_extension_duration - Session延长时间(秒) │ │
│ │ - timestamp_tolerance_seconds - 时间戳容忍秒数 │ │
│ │ - max_login_attempts - 最大登录失败次数 │ │
│ │ - login_lockout_minutes - 登录锁定分钟数 │ │
│ │ - key_sync_interval_seconds - 密钥同步间隔秒数 │ │
│ │ - 其他客户端相关配置... │ │
│ │ │ │
│ │ 客户端本地不存储任何配置,全部通过心跳动态获取 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 自动退出登录触发条件与处理 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【触发条件】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 触发场景 │ 错误码 │ 说明 │ │
│ │ ------------------------------│--------│------------------------│ │
│ │ 1. session过期/无效 │ 401 │ 会话已过期 │ │
│ │ 2. 机器码不匹配 │ 403 │ 账号在其他设备登录 │ │
│ │ 3. 卡密已过期 │ 403 │ 卡密到期 │ │
│ │ 4. 账户被禁用 │ 403 │ 账号被禁用 │ │
│ │ 5. 心跳服务异常(累计超限) │ - │ 连续失败超过配置次数 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【心跳失败处理流程】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 客户端检测到心跳失败 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ 失败次数 + 1 │ │ │
│ │ └───────────┬─────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ 失败次数 >= max_heartbeat_failures? │ │ │
│ │ └──────────────────┬──────────────────────┘ │ │
│ │ │ │ │
│ │ ┌───────────┴───────────┐ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────┐ ┌─────────────────────┐ │ │
│ │ │ 是 │ │ 否 │ │ │
│ │ │ 触发退出 │ │ 继续执行 │ │ │
│ │ └──────┬──────┘ └─────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ 执行退出登录流程 │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【退出登录执行步骤】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 停止心跳定时器 │ │
│ │ 2. 停止密钥同步定时器 │ │
│ │ 3. 关闭所有子线程 │ │
│ │ 4. 清除本地会话缓存 (session_id, user_id) │ │
│ │ 5. 跳转到登录页面 │ │
│ │ 6. 显示友好的错误提示: │ │
│ │ - session过期: "会话已过期,请重新登录" │ │
│ │ - 机器码不匹配: "账号已在其他设备登录" │ │
│ │ - 卡密过期: "卡密已到期,请前往续费" │ │
│ │ - 心跳异常: "服务连接失败,请检查网络后重试" │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【心跳失败次数配置】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 配置项: max_heartbeat_failures │ │
│ │ 来源: system_config表 │ │
│ │ 默认值: 100 │ │
│ │ 作用: 连续心跳失败超过此次数则自动退出登录 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 客户端配置管理规范 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【配置获取方式】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 客户端启动时: │ │
│ │ - 登录成功后首次调用心跳获取完整配置 │ │
│ │ │ │
│ │ 2. 心跳响应中获取: │ │
│ │ - 每次心跳都返回最新的完整配置 │ │
│ │ - 客户端更新本地配置缓存 │ │
│ │ - 无需重启应用,配置立即生效 │ │
│ │ │ │
│ │ 3. 配置变更后: │ │
│ │ - 管理员在backend-admin中修改system_config │ │
│ │ - 下次心跳时客户端自动获取最新配置 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【配置存储规则】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ - 客户端本地不持久化任何配置 │ │
│ │ - 所有配置从心跳响应中动态获取 │ │
│ │ - 配置仅在内存中缓存,客户端重启后重新获取 │ │
│ │ - 以心跳响应中的配置为准 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【配置优先级】 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. system_config表中的配置 (最高优先级) │ │
│ │ 2. 心跳响应返回的配置覆盖本地缓存 │ │
│ │ 3. 客户端代码中无硬编码配置 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
文档结束