Files
angjustinl 8f9a6e7f9d feat(login, zulip): 引入 JWT 验证并重构 API 密钥管理
### 详细变更描述

* **修复 JWT 签名冲突**:重构 `LoginService.generateTokenPair()`,移除载荷(Payload)中的 `iss` (issuer) 与 `aud` (audience) 字段,解决签名校验失败的问题。
* **统一验证逻辑**:更新 `ZulipService` 以调用 `LoginService.verifyToken()`,消除重复的 JWT 校验代码,确保逻辑单一职责化(Single Responsibility)。
* **修复硬编码 API 密钥问题**:消息发送功能不再依赖静态配置,改为从 Redis 动态读取用户真实的 API 密钥。
* **解耦依赖注入**:在 `ZulipModule` 中注入 `AuthModule` 依赖,以支持标准的 Token 验证流程。
* **完善技术文档**:补充了关于 JWT 验证流程及 API 密钥管理逻辑的详细文档。
* **新增测试工具**:添加 `test-get-messages.js` 脚本,用于验证通过 WebSocket 接收消息的功能。
* **更新自动化脚本**:同步更新了 API 密钥验证及用户注册校验的快速测试脚本。
* **端到端功能验证**:确保消息发送逻辑能够正确映射并调用用户真实的 Zulip API 密钥。
2026-01-06 18:51:37 +08:00

217 lines
8.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
游戏属性: 2d社交属性, 无战斗的社群mmo游戏
核心目的(游戏内外无缝互通): 不玩这个游戏但是在zulip的社群成员, 也可以跨平台和游戏内的成员聊天
核心设计理念:
Stream (流) -> Topic (话题) 线程模型,天然契合 MMO 中的 Zone (区域) -> Context (情境) 逻辑
我们需要解决的核心问题是如何将2D 空间位置Game State映射到Zulip 的信息组织形式Message State同时利用 Zulip 的 API Key 机制完成无缝认证
---
1. 核心逻辑架构 (The Core Logic)
在设计 API 之前,我们需要定义 mappings映射关系
- Game World / Map ←→ Zulip Stream (e.g., #Novice_Village)
- Interactive Object / Event ←→ Zulip Topic (e.g., Notice Board, Tavern Gossip)
- Whisper / Party ←→ Zulip Private Message
---
架构图示:
Client (Game) $$\xrightarrow{\text{Game Token}}$$ Game Middleware API $$\xrightarrow{\text{Zulip API Key}}$$ Zulip Server
$$Client (Godot) \xleftrightarrow{\text{WebSocket}} Node.js Server \xleftrightarrow{\text{REST/Long-Poll}} Zulip Server$$
设计理由:不建议让客户端直接直连 Zulip。我们需要一层中间件Middleware来控制权限、注入游戏数据如玩家坐标、当前的动作状态并防止用户在该 API Key 下进行非游戏允许的 Zulip 操作(如随意创建 Stream
---
2. 设计思路一: "统一网关"Unified Gateway
2.1 详细数据流设计 (Data Flow)
我们需要在 Node.js 中维护一个 Session Manager。
A. 登录与握手 (Initialization)
1. Godot: 发送登录包 {"type": "login", "token": "user_game_token"}。
2. Node.js:
- 验证游戏 Token。
- 查找该用户的 Zulip API Key通常存储在数据库中或者首次登录时让用户提供
- 关键步骤: Node.js 服务器为该特定用户实例化一个 Zulip Client并向 Zulip 申请注册一个 Event Queue。
- 将 Socket_ID 与 Zulip_Queue_ID 绑定。
B. 发送消息 (Upstream: Godot -> Node -> Zulip)
1. Godot: 玩家输入 "Hello"Godot 通过 WebSocket 发送简化的包:
2. JSON
{
"t": "chat",
"content": "Hello",
"scope": "local" // 或者 "topic_name"
}
1. Node.js:
- 收到包,解析出这是聊天请求。
- 上下文注入: Node 知道玩家当前在 Map_101 (对应 Zulip Stream #Tavern)。
- API 调用: Node 使用该用户的 Zulip Client调用 Zulip API 发送消息到 #Tavern
- 优势: 这里可以做风控比如禁止发脏话、频率限制Godot 端根本无法绕过。
C. 接收消息 (Downstream: Zulip -> Node -> Godot)
1. Node.js:
- 服务器内部有一个循环(或者异步监听器),轮询 Zulip 的事件队列。
- 收到 Zulip 的 message 事件User_B 在 #Tavern 说了 "Hi"。
- 空间过滤: Node 检查当前连接的所有 WebSocket找出所有位于 Map_101 的玩家。
- 广播: 将消息打包成游戏协议,通过 WebSocket 推送给这些玩家:
2. JSON
{
"t": "chat_render",
"from": "User_B",
"txt": "Hi",
"bubble": true
}
1. Godot: 收到包,直接调用 show_bubble()。
---
2.3 这个方案的权衡分析 (Trade-off Analysis)
这种改变带来的本质变化:
优势 (The Wins)
1. 客户端极度简化 (Thin Client):
- Godot 里不需要写 HTTP Request不需要处理 Long Polling 的异常断连,不需要解析复杂的 JSON 结构。
- Godot 只需要处理 on_websocket_packet_received。
2. 安全性 (Security):
- Zulip API Key 永不下发: 用户的 Zulip 凭证永远只保存在服务器端。如果客户端直接拿 Key黑客可以通过解包 Godot 拿到 Key 然后去 Zulip 乱发消息。
- 位置欺诈完全消除: 因为 Stream 的选择权在 Node 手里,玩家无法做到“人在新手村,却往高等级区域频道发消息”。
3. 协议统一:
- 不再需要处理 HTTP (Zulip) 和 WebSocket (Game) 两种并发逻辑。网络层代码减少一半。
---
## 3. JWT Token 验证和 API Key 管理 (v1.1.0 更新)
### 3.1 用户注册和 API Key 生成流程
当用户注册游戏账号时,系统会自动创建对应的 Zulip 账号并获取 API Key
```
用户注册 (POST /auth/register)
1. 创建游戏账号 (LoginService.register)
2. 初始化 Zulip 管理员客户端
3. 创建 Zulip 账号 (ZulipAccountService.createZulipAccount)
- 使用相同的邮箱和密码
- 调用 Zulip API: POST /api/v1/users
4. 获取 API Key (ZulipAccountService.generateApiKeyForUser)
- 使用 fetch_api_key 端点(固定的、基于密码的 Key
- 注意:不使用 regenerate_api_key会生成新 Key
5. 加密存储 API Key (ApiKeySecurityService.storeApiKey)
- 使用 AES-256-GCM 加密
- 存储到 Redis: zulip:api_key:{userId}
6. 创建账号关联记录 (ZulipAccountsRepository)
- 存储 gameUserId ↔ zulipUserId 映射
7. 生成 JWT Token (LoginService.generateTokenPair)
- 包含用户信息sub, username, email, role
- 返回 access_token 和 refresh_token
```
### 3.2 JWT Token 验证流程
当用户通过 WebSocket 登录游戏时,系统会验证 JWT Token 并获取 API Key
```
WebSocket 登录 (login 消息)
1. ZulipService.validateGameToken(token)
2. 调用 LoginService.verifyToken(token, 'access')
- 验证签名、过期时间、载荷
- 提取用户信息userId, username, email
3. 从 Redis 获取 API Key (ApiKeySecurityService.getApiKey)
- 解密存储的 API Key
- 更新访问计数和时间
4. 创建 Zulip 客户端 (ZulipClientPoolService.createUserClient)
- 使用真实的用户 API Key
- 注册事件队列
5. 创建游戏会话 (SessionManagerService.createSession)
- 绑定 socketId ↔ zulipQueueId
- 记录用户位置信息
6. 返回登录成功
```
### 3.3 消息发送流程(使用正确的 API Key
```
发送聊天消息 (chat 消息)
1. ZulipService.sendChatMessage()
2. 获取会话信息 (SessionManagerService.getSession)
- 获取 userId 和当前位置
3. 上下文注入 (SessionManagerService.injectContext)
- 根据位置确定目标 Stream/Topic
4. 消息验证 (MessageFilterService.validateMessage)
- 内容过滤、频率限制
5. 发送到 Zulip (ZulipClientPoolService.sendMessage)
- 使用用户的真实 API Key
- 调用 Zulip API: POST /api/v1/messages
6. 返回发送结果
```
### 3.4 关键修复说明
**问题 1: JWT Token 签名冲突**
- **原因**: payload 中包含 `iss``aud`,但 `jwtService.signAsync()` 也通过 options 传递这些值
- **修复**: 从 payload 中移除 `iss``aud`,只通过 options 传递
- **文件**: `src/business/auth/services/login.service.ts`
**问题 2: 使用硬编码的旧 API Key**
- **原因**: `ZulipService.validateGameToken()` 使用硬编码的测试 API Key
- **修复**: 从 Redis 读取用户注册时存储的真实 API Key
- **文件**: `src/business/zulip/zulip.service.ts`
**问题 3: 重复实现 JWT 验证逻辑**
- **原因**: `ZulipService` 自己实现了 JWT 解析
- **修复**: 复用 `LoginService.verifyToken()` 方法
- **文件**: `src/business/zulip/zulip.service.ts`, `src/business/zulip/zulip.module.ts`
### 3.5 API Key 安全机制
**加密存储**:
- 使用 AES-256-GCM 算法加密
- 加密密钥从环境变量 `ZULIP_API_KEY_ENCRYPTION_KEY` 读取
- 存储格式:`{encryptedKey, iv, authTag, createdAt, updatedAt, accessCount}`
**访问控制**:
- 频率限制:每分钟最多 60 次访问
- 访问日志:记录每次访问的时间和次数
- 安全事件:记录所有关键操作(存储、访问、更新、删除)
**环境变量配置**:
```bash
# 生成 64 字符的十六进制密钥32 字节 = 256 位)
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# 在 .env 文件中配置
ZULIP_API_KEY_ENCRYPTION_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
```
### 3.6 测试验证
使用测试脚本验证功能:
```bash
# 测试注册用户的 Zulip 集成
node docs/systems/zulip/quick_tests/test-registered-user.js
# 验证 API Key 一致性
node docs/systems/zulip/quick_tests/verify-api-key.js
```
**预期结果**:
- ✅ WebSocket 连接成功
- ✅ JWT Token 验证通过
- ✅ 从 Redis 获取正确的 API Key
- ✅ 消息成功发送到 Zulip
---