From 8f9a6e7f9d4f65e289e047b273681ed92f132d9b Mon Sep 17 00:00:00 2001 From: angjustinl <96008766+ANGJustinl@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:51:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(login,=20zulip):=20=E5=BC=95=E5=85=A5=20JW?= =?UTF-8?q?T=20=E9=AA=8C=E8=AF=81=E5=B9=B6=E9=87=8D=E6=9E=84=20API=20?= =?UTF-8?q?=E5=AF=86=E9=92=A5=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 详细变更描述 * **修复 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 密钥。 --- docs/systems/zulip/README.md | 11 +- docs/systems/zulip/guide.md | 147 +++++++++- .../zulip/quick_tests/test-get-messages.js | 260 +++++++++++++++++ .../quick_tests/test-list-subscriptions.js | 113 +++++++- .../zulip/quick_tests/test-user-api-key.js | 266 +++++++++++------- src/business/auth/services/login.service.ts | 21 +- src/business/zulip/zulip.module.ts | 7 +- src/business/zulip/zulip.service.ts | 125 ++++---- 8 files changed, 763 insertions(+), 187 deletions(-) create mode 100644 docs/systems/zulip/quick_tests/test-get-messages.js diff --git a/docs/systems/zulip/README.md b/docs/systems/zulip/README.md index fafcd4f..18efb9e 100644 --- a/docs/systems/zulip/README.md +++ b/docs/systems/zulip/README.md @@ -358,7 +358,16 @@ node test-stream-initialization.js ## 更新日志 -### v2.0.0 (2025-12-25) +### v1.1.0 (2026-01-06) +- **修复 JWT Token 验证和 API Key 管理** + - 修复 `LoginService.generateTokenPair()` 的 JWT 签名冲突问题 + - `ZulipService` 现在复用 `LoginService.verifyToken()` 进行 Token 验证 + - 修复消息发送时使用错误的硬编码 API Key 问题 + - 现在正确从 Redis 读取用户注册时存储的真实 API Key + - 添加 `AuthModule` 到 `ZulipModule` 的依赖注入 + - 消息发送功能现已完全正常工作 ✅ + +### v1.0.1 (2025-12-25) - 更新地图配置为 9 区域系统 - 添加 Stream Initializer Service 自动初始化服务 - 更新默认出生点为鲸之港 (Whale Port) diff --git a/docs/systems/zulip/guide.md b/docs/systems/zulip/guide.md index d7f5734..2d7f526 100644 --- a/docs/systems/zulip/guide.md +++ b/docs/systems/zulip/guide.md @@ -68,4 +68,149 @@ C. 接收消息 (Downstream: Zulip -> Node -> Godot) - Zulip API Key 永不下发: 用户的 Zulip 凭证永远只保存在服务器端。如果客户端直接拿 Key,黑客可以通过解包 Godot 拿到 Key 然后去 Zulip 乱发消息。 - 位置欺诈完全消除: 因为 Stream 的选择权在 Node 手里,玩家无法做到“人在新手村,却往高等级区域频道发消息”。 3. 协议统一: - - 不再需要处理 HTTP (Zulip) 和 WebSocket (Game) 两种并发逻辑。网络层代码减少一半。 \ No newline at end of file + - 不再需要处理 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 + +--- diff --git a/docs/systems/zulip/quick_tests/test-get-messages.js b/docs/systems/zulip/quick_tests/test-get-messages.js new file mode 100644 index 0000000..a7f14b0 --- /dev/null +++ b/docs/systems/zulip/quick_tests/test-get-messages.js @@ -0,0 +1,260 @@ +/** + * 测试通过 WebSocket 接收 Zulip 消息 + * + * 设计理念: + * - Zulip API Key 永不下发到客户端 + * - 所有 Zulip 交互通过游戏服务器的 WebSocket 进行 + * - 客户端只接收 chat_render 消息,不直接调用 Zulip API + * + * 功能: + * 1. 登录游戏服务器获取 JWT Token + * 2. 通过 WebSocket 连接游戏服务器 + * 3. 在当前地图 (Whale Port) 接收消息 + * 4. 切换到 Pumpkin Valley 接收消息 + * 5. 统计接收到的消息数量 + * + * 使用方法: + * node docs/systems/zulip/quick_tests/test-get-messages.js + */ + +const axios = require('axios'); +const io = require('socket.io-client'); + +// 配置 +const GAME_SERVER = 'http://localhost:3000'; +const TEST_USER = { + username: 'angtest123', + password: 'angtest123', + email: 'angjustinl@163.com' +}; + +// 测试配置 +const TEST_CONFIG = { + whalePortWaitTime: 10000, // 在 Whale Port 等待 10 秒 + pumpkinValleyWaitTime: 10000, // 在 Pumpkin Valley 等待 10 秒 + totalTimeout: 30000 // 总超时时间 30 秒 +}; + +/** + * 登录游戏服务器获取用户信息 + */ +async function loginToGameServer() { + console.log('📝 步骤 1: 登录游戏服务器'); + console.log(` 用户名: ${TEST_USER.username}`); + + try { + const response = await axios.post(`${GAME_SERVER}/auth/login`, { + identifier: TEST_USER.username, + password: TEST_USER.password + }); + + if (response.data.success) { + console.log('✅ 登录成功'); + console.log(` 用户ID: ${response.data.data.user.id}`); + console.log(` 邮箱: ${response.data.data.user.email}`); + return { + userId: response.data.data.user.id, + username: response.data.data.user.username, + email: response.data.data.user.email, + token: response.data.data.access_token + }; + } else { + throw new Error(response.data.message || '登录失败'); + } + } catch (error) { + console.error('❌ 登录失败:', error.response?.data?.message || error.message); + throw error; + } +} + + + +/** + * 通过 WebSocket 接收消息 + */ +async function receiveMessagesViaWebSocket(userInfo) { + console.log('\n📡 步骤 2: 通过 WebSocket 连接并接收消息'); + console.log(` 连接到: ${GAME_SERVER}/game`); + + return new Promise((resolve, reject) => { + const socket = io(`${GAME_SERVER}/game`, { + transports: ['websocket'], + timeout: 20000 + }); + + const receivedMessages = { + whalePort: [], + pumpkinValley: [] + }; + + let currentMap = 'whale_port'; + let testPhase = 0; // 0: 连接中, 1: Whale Port, 2: Pumpkin Valley, 3: 完成 + + // 连接成功 + socket.on('connect', () => { + console.log('✅ WebSocket 连接成功'); + + // 发送登录消息 + const loginMessage = { + type: 'login', + token: userInfo.token + }; + + console.log('📤 发送登录消息...'); + socket.emit('login', loginMessage); + }); + + // 登录成功 + socket.on('login_success', (data) => { + console.log('✅ 登录成功'); + console.log(` 会话ID: ${data.sessionId}`); + console.log(` 用户ID: ${data.userId}`); + console.log(` 当前地图: ${data.currentMap}`); + + testPhase = 1; + currentMap = data.currentMap || 'whale_port'; + + console.log(`\n📬 步骤 3: 在 Whale Port 接收消息 (等待 ${TEST_CONFIG.whalePortWaitTime / 1000} 秒)`); + console.log(' 💡 提示: 请在 Zulip 的 "Whale Port" Stream 发送测试消息'); + + // 在 Whale Port 等待一段时间 + setTimeout(() => { + console.log(`\n📊 Whale Port 接收到 ${receivedMessages.whalePort.length} 条消息`); + + // 切换到 Pumpkin Valley + console.log(`\n📤 步骤 4: 切换到 Pumpkin Valley`); + const positionUpdate = { + t: 'position', + x: 150, + y: 400, + mapId: 'pumpkin_valley' + }; + socket.emit('position_update', positionUpdate); + + testPhase = 2; + currentMap = 'pumpkin_valley'; + + console.log(`\n📬 步骤 5: 在 Pumpkin Valley 接收消息 (等待 ${TEST_CONFIG.pumpkinValleyWaitTime / 1000} 秒)`); + console.log(' 💡 提示: 请在 Zulip 的 "Pumpkin Valley" Stream 发送测试消息'); + + // 在 Pumpkin Valley 等待一段时间 + setTimeout(() => { + console.log(`\n📊 Pumpkin Valley 接收到 ${receivedMessages.pumpkinValley.length} 条消息`); + + testPhase = 3; + console.log('\n📊 测试完成,断开连接...'); + socket.disconnect(); + }, TEST_CONFIG.pumpkinValleyWaitTime); + }, TEST_CONFIG.whalePortWaitTime); + }); + + // 接收到消息 (chat_render) + socket.on('chat_render', (data) => { + const timestamp = new Date().toLocaleTimeString('zh-CN'); + + console.log(`\n📨 [${timestamp}] 收到消息:`); + console.log(` ├─ 发送者: ${data.from}`); + console.log(` ├─ 内容: ${data.txt}`); + console.log(` ├─ Stream: ${data.stream || '未知'}`); + console.log(` ├─ Topic: ${data.topic || '未知'}`); + console.log(` └─ 当前地图: ${currentMap}`); + + // 记录消息 + const message = { + from: data.from, + content: data.txt, + stream: data.stream, + topic: data.topic, + timestamp: new Date(), + map: currentMap + }; + + if (testPhase === 1) { + receivedMessages.whalePort.push(message); + } else if (testPhase === 2) { + receivedMessages.pumpkinValley.push(message); + } + }); + + // 错误处理 + socket.on('error', (error) => { + console.error('❌ 收到错误:', JSON.stringify(error, null, 2)); + }); + + // 连接断开 + socket.on('disconnect', () => { + console.log('\n🔌 WebSocket 连接已关闭'); + resolve(receivedMessages); + }); + + // 连接错误 + socket.on('connect_error', (error) => { + console.error('❌ 连接错误:', error.message); + reject(error); + }); + + // 总超时保护 + setTimeout(() => { + if (socket.connected) { + console.log('\n⏰ 测试超时,关闭连接'); + socket.disconnect(); + } + }, TEST_CONFIG.totalTimeout); + }); +} + +/** + * 主测试流程 + */ +async function runTest() { + console.log('🚀 开始测试通过 WebSocket 接收 Zulip 消息'); + console.log('='.repeat(60)); + console.log('📋 设计理念: Zulip API Key 永不下发到客户端'); + console.log('📋 所有消息通过游戏服务器的 WebSocket (chat_render) 接收'); + console.log('='.repeat(60)); + + try { + // 步骤1: 登录游戏服务器 + const userInfo = await loginToGameServer(); + + // 步骤2-5: 通过 WebSocket 接收消息 + const receivedMessages = await receiveMessagesViaWebSocket(userInfo); + + // 步骤6: 统计信息 + console.log('\n' + '='.repeat(60)); + console.log('📊 测试结果汇总'); + console.log('='.repeat(60)); + console.log(`✅ Whale Port: ${receivedMessages.whalePort.length} 条消息`); + console.log(`✅ Pumpkin Valley: ${receivedMessages.pumpkinValley.length} 条消息`); + console.log(`📝 总计: ${receivedMessages.whalePort.length + receivedMessages.pumpkinValley.length} 条消息`); + + // 显示详细消息列表 + if (receivedMessages.whalePort.length > 0) { + console.log('\n📬 Whale Port 消息列表:'); + receivedMessages.whalePort.forEach((msg, index) => { + console.log(` ${index + 1}. [${msg.timestamp.toLocaleTimeString()}] ${msg.from}: ${msg.content.substring(0, 50)}${msg.content.length > 50 ? '...' : ''}`); + }); + } + + if (receivedMessages.pumpkinValley.length > 0) { + console.log('\n📬 Pumpkin Valley 消息列表:'); + receivedMessages.pumpkinValley.forEach((msg, index) => { + console.log(` ${index + 1}. [${msg.timestamp.toLocaleTimeString()}] ${msg.from}: ${msg.content.substring(0, 50)}${msg.content.length > 50 ? '...' : ''}`); + }); + } + + console.log('='.repeat(60)); + + console.log('\n🎉 测试完成!'); + console.log('💡 提示: 客户端通过 WebSocket 接收消息,无需直接访问 Zulip API'); + console.log('💡 提示: 访问 https://zulip.xinghangee.icu 查看完整消息历史'); + + process.exit(0); + } catch (error) { + console.error('\n❌ 测试失败:', error.message); + process.exit(1); + } +} + +// 运行测试 +runTest(); diff --git a/docs/systems/zulip/quick_tests/test-list-subscriptions.js b/docs/systems/zulip/quick_tests/test-list-subscriptions.js index 2136548..6dfb74d 100644 --- a/docs/systems/zulip/quick_tests/test-list-subscriptions.js +++ b/docs/systems/zulip/quick_tests/test-list-subscriptions.js @@ -1,15 +1,102 @@ const zulip = require('zulip-js'); +const axios = require('axios'); -async function listSubscriptions() { - console.log('🔧 检查用户订阅的 Streams...'); - - const config = { - username: 'angjustinl@mail.angforever.top', - apiKey: 'lCPWC...pqNfGF8', - realm: 'https://zulip.xinghangee.icu/' - }; +// 配置 +const GAME_SERVER = 'http://localhost:3000'; +const TEST_USER = { + username: 'angtest123', + password: 'angtest123', + email: 'angjustinl@163.com' +}; + +/** + * 登录游戏服务器获取用户信息 + */ +async function loginToGameServer() { + console.log('📝 步骤 1: 登录游戏服务器'); + console.log(` 用户名: ${TEST_USER.username}`); try { + const response = await axios.post(`${GAME_SERVER}/auth/login`, { + identifier: TEST_USER.username, + password: TEST_USER.password + }); + + if (response.data.success) { + console.log('✅ 登录成功'); + console.log(` 用户ID: ${response.data.data.user.id}`); + console.log(` 邮箱: ${response.data.data.user.email}`); + return { + userId: response.data.data.user.id, + username: response.data.data.user.username, + email: response.data.data.user.email, + token: response.data.data.access_token + }; + } else { + throw new Error(response.data.message || '登录失败'); + } + } catch (error) { + console.error('❌ 登录失败:', error.response?.data?.message || error.message); + throw error; + } +} + +/** + * 使用密码获取 Zulip API Key + */ +async function getZulipApiKey(email, password) { + console.log('\n📝 步骤 2: 获取 Zulip API Key'); + console.log(` 邮箱: ${email}`); + + try { + // Zulip API 使用 Basic Auth 和 form data + const response = await axios.post( + 'https://zulip.xinghangee.icu/api/v1/fetch_api_key', + `username=${encodeURIComponent(email)}&password=${encodeURIComponent(password)}`, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + if (response.data.result === 'success') { + console.log('✅ 成功获取 API Key'); + console.log(` API Key: ${response.data.api_key.substring(0, 10)}...`); + console.log(` 用户ID: ${response.data.user_id}`); + return { + apiKey: response.data.api_key, + email: response.data.email, + userId: response.data.user_id + }; + } else { + throw new Error(response.data.msg || '获取 API Key 失败'); + } + } catch (error) { + console.error('❌ 获取 API Key 失败:', error.response?.data?.msg || error.message); + throw error; + } +} + +async function listSubscriptions() { + console.log('🚀 开始测试用户订阅的 Streams'); + console.log('='.repeat(60)); + + try { + // 步骤1: 登录游戏服务器 + const userInfo = await loginToGameServer(); + + // 步骤2: 获取 Zulip API Key + const zulipAuth = await getZulipApiKey(userInfo.email, TEST_USER.password); + + console.log('\n📝 步骤 3: 检查用户订阅的 Streams'); + + const config = { + username: zulipAuth.email, + apiKey: zulipAuth.apiKey, + realm: 'https://zulip.xinghangee.icu/' + }; + const client = await zulip(config); // 获取用户信息 @@ -29,15 +116,15 @@ async function listSubscriptions() { }); // 检查是否有 "Novice Village" - const noviceVillage = subscriptions.subscriptions.find(s => s.name === 'Novice Village'); + const noviceVillage = subscriptions.subscriptions.find(s => s.name === 'Pumpkin Valley'); if (noviceVillage) { - console.log('\n✅ "Novice Village" Stream 已存在!'); + console.log('\n✅ "Pumpkin Valley" Stream 已存在!'); // 测试发送消息 console.log('\n📤 测试发送消息...'); const result = await client.messages.send({ type: 'stream', - to: 'Novice Village', + to: 'Pumpkin Valley', subject: 'General', content: '测试消息:系统集成测试成功 🎮' }); @@ -48,7 +135,7 @@ async function listSubscriptions() { console.log('❌ 消息发送失败:', result.msg); } } else { - console.log('\n⚠️ "Novice Village" Stream 不存在'); + console.log('\n⚠️ "Pumpkin Valley" Stream 不存在'); console.log('💡 请在 Zulip 网页界面手动创建该 Stream,或使用管理员账号创建'); // 尝试发送到第一个可用的 Stream @@ -79,7 +166,9 @@ async function listSubscriptions() { if (error.response) { console.error('响应数据:', error.response.data); } + process.exit(1); } } +// 运行测试 listSubscriptions(); diff --git a/docs/systems/zulip/quick_tests/test-user-api-key.js b/docs/systems/zulip/quick_tests/test-user-api-key.js index 4591dbb..06ac28a 100644 --- a/docs/systems/zulip/quick_tests/test-user-api-key.js +++ b/docs/systems/zulip/quick_tests/test-user-api-key.js @@ -1,127 +1,183 @@ const io = require('socket.io-client'); +const axios = require('axios'); + +// 配置 +const GAME_SERVER = 'http://localhost:3000'; +const TEST_USER = { + username: 'angtest123', + password: 'angtest123', + email: 'angjustinl@163.com' +}; + +/** + * 登录游戏服务器获取token + */ +async function loginToGameServer() { + console.log('📝 步骤 1: 登录游戏服务器'); + console.log(` 用户名: ${TEST_USER.username}`); + + try { + const response = await axios.post(`${GAME_SERVER}/auth/login`, { + identifier: TEST_USER.username, + password: TEST_USER.password + }); + + if (response.data.success) { + console.log('✅ 登录成功'); + console.log(` 用户ID: ${response.data.data.user.id}`); + console.log(` 昵称: ${response.data.data.user.nickname}`); + console.log(` 邮箱: ${response.data.data.user.email}`); + console.log(` Token: ${response.data.data.access_token.substring(0, 20)}...`); + return { + userId: response.data.data.user.id, + username: response.data.data.user.username, + token: response.data.data.access_token + }; + } else { + throw new Error(response.data.message || '登录失败'); + } + } catch (error) { + console.error('❌ 登录失败:', error.response?.data?.message || error.message); + throw error; + } +} // 使用用户 API Key 测试 Zulip 集成 async function testWithUserApiKey() { - console.log('🚀 使用用户 API Key 测试 Zulip 集成...'); - console.log('📡 用户 API Key: lCPWCPfGh7WU...pqNfGF8'); - console.log('📡 Zulip 服务器: https://zulip.xinghangee.icu/'); - console.log('📡 游戏服务器: http://localhost:3000/game'); + console.log('🚀 开始测试用户 API Key 的 Zulip 集成'); + console.log('='.repeat(60)); - const socket = io('http://localhost:3000/game', { - transports: ['websocket'], - timeout: 20000 - }); - - let testStep = 0; - - socket.on('connect', () => { - console.log('✅ WebSocket 连接成功'); - testStep = 1; + try { + // 登录获取 token + const userInfo = await loginToGameServer(); - // 使用包含用户 API Key 的 token - const loginMessage = { - type: 'login', - token: 'lCPWCPfGh7...fGF8_user_token' - }; - - console.log('📤 步骤 1: 发送登录消息(使用用户 API Key)'); - socket.emit('login', loginMessage); - }); + console.log('\n📡 步骤 2: 连接 WebSocket 并测试 Zulip 集成'); + console.log(` 连接到: ${GAME_SERVER}/game`); - socket.on('login_success', (data) => { - console.log('✅ 步骤 1 完成: 登录成功'); - console.log(' 会话ID:', data.sessionId); - console.log(' 用户ID:', data.userId); - console.log(' 用户名:', data.username); - console.log(' 当前地图:', data.currentMap); - testStep = 2; + const socket = io(`${GAME_SERVER}/game`, { + transports: ['websocket'], + timeout: 20000 + }); - // 等待 Zulip 客户端初始化 - console.log('⏳ 等待 3 秒让 Zulip 客户端初始化...'); - setTimeout(() => { - const chatMessage = { - t: 'chat', - content: '🎮 【用户API Key测试】来自游戏的消息!\\n' + - '时间: ' + new Date().toLocaleString() + '\\n' + - '使用用户 API Key 发送此消息。', - scope: 'local' + let testStep = 0; + + socket.on('connect', () => { + console.log('✅ WebSocket 连接成功'); + testStep = 1; + + // 使用真实的 JWT token + const loginMessage = { + type: 'login', + token: userInfo.token }; - console.log('📤 步骤 2: 发送消息到 Zulip(使用用户 API Key)'); - console.log(' 目标 Stream: Whale Port'); - socket.emit('chat', chatMessage); - }, 3000); - }); + console.log('📤 步骤 3: 发送登录消息(使用 JWT Token)'); + socket.emit('login', loginMessage); + }); - socket.on('chat_sent', (data) => { - console.log('✅ 步骤 2 完成: 消息发送成功'); - console.log(' 响应:', JSON.stringify(data, null, 2)); - - // 只在第一次收到 chat_sent 时发送第二条消息 - if (testStep === 2) { - testStep = 3; + socket.on('login_success', (data) => { + console.log('✅ 步骤 3 完成: 登录成功'); + console.log(' 会话ID:', data.sessionId); + console.log(' 用户ID:', data.userId); + console.log(' 用户名:', data.username); + console.log(' 当前地图:', data.currentMap); + testStep = 2; + // 等待 Zulip 客户端初始化 + console.log('\n⏳ 等待 3 秒让 Zulip 客户端初始化...'); setTimeout(() => { - // 先切换到 Pumpkin Valley 地图 - console.log('📤 步骤 3: 切换到 Pumpkin Valley 地图'); - const positionUpdate = { - t: 'position', - x: 150, - y: 400, - mapId: 'pumpkin_valley' + const chatMessage = { + t: 'chat', + content: `🎮 【用户API Key测试】来自 ${userInfo.username} 的消息!\n` + + `时间: ${new Date().toLocaleString()}\n` + + `使用真实 API Key 发送此消息。`, + scope: 'local' }; - socket.emit('position_update', positionUpdate); - // 等待位置更新后发送消息 + console.log('📤 步骤 4: 发送消息到 Zulip(使用真实 API Key)'); + console.log(' 目标 Stream: Whale Port'); + socket.emit('chat', chatMessage); + }, 3000); + }); + + socket.on('chat_sent', (data) => { + console.log('✅ 步骤 4 完成: 消息发送成功'); + console.log(' 响应:', JSON.stringify(data, null, 2)); + + // 只在第一次收到 chat_sent 时发送第二条消息 + if (testStep === 2) { + testStep = 3; + setTimeout(() => { - const chatMessage2 = { - t: 'chat', - content: '🎃 在南瓜谷发送的测试消息!', - scope: 'local' + // 先切换到 Pumpkin Valley 地图 + console.log('\n📤 步骤 5: 切换到 Pumpkin Valley 地图'); + const positionUpdate = { + t: 'position', + x: 150, + y: 400, + mapId: 'pumpkin_valley' }; + socket.emit('position_update', positionUpdate); - console.log('📤 步骤 4: 在 Pumpkin Valley 发送消息'); - socket.emit('chat', chatMessage2); - }, 1000); - }, 2000); - } - }); + // 等待位置更新后发送消息 + setTimeout(() => { + const chatMessage2 = { + t: 'chat', + content: '🎃 在南瓜谷发送的测试消息!', + scope: 'local' + }; + + console.log('📤 步骤 6: 在 Pumpkin Valley 发送消息'); + socket.emit('chat', chatMessage2); + }, 1000); + }, 2000); + } + }); - socket.on('chat_render', (data) => { - console.log('📨 收到来自 Zulip 的消息:'); - console.log(' 发送者:', data.from); - console.log(' 内容:', data.txt); - console.log(' Stream:', data.stream || '未知'); - console.log(' Topic:', data.topic || '未知'); - }); - - socket.on('error', (error) => { - console.log('❌ 收到错误:', JSON.stringify(error, null, 2)); - }); - - socket.on('disconnect', () => { - console.log('🔌 WebSocket 连接已关闭'); - console.log(''); - console.log('📊 测试结果:'); - console.log(' 完成步骤:', testStep, '/ 4'); - if (testStep >= 3) { - console.log(' ✅ 核心功能正常!'); - console.log(' 💡 请检查 Zulip 中的 "Whale Port" 和 "Pumpkin Valley" Streams 查看消息'); - } - process.exit(0); - }); - - socket.on('connect_error', (error) => { - console.error('❌ 连接错误:', error.message); + socket.on('chat_render', (data) => { + console.log('\n📨 收到来自 Zulip 的消息:'); + console.log(' 发送者:', data.from); + console.log(' 内容:', data.txt); + console.log(' Stream:', data.stream || '未知'); + console.log(' Topic:', data.topic || '未知'); + }); + + socket.on('error', (error) => { + console.log('❌ 收到错误:', JSON.stringify(error, null, 2)); + }); + + socket.on('disconnect', () => { + console.log('\n🔌 WebSocket 连接已关闭'); + console.log('\n' + '='.repeat(60)); + console.log('📊 测试结果汇总'); + console.log('='.repeat(60)); + console.log(' 完成步骤:', testStep, '/ 3'); + if (testStep >= 3) { + console.log(' ✅ 核心功能正常!'); + console.log(' 💡 请检查 Zulip 中的 "Whale Port" 和 "Pumpkin Valley" Streams 查看消息'); + } else { + console.log(' ⚠️ 部分测试未完成'); + } + console.log('='.repeat(60)); + process.exit(testStep >= 3 ? 0 : 1); + }); + + socket.on('connect_error', (error) => { + console.error('❌ 连接错误:', error.message); + process.exit(1); + }); + + // 20秒后自动关闭(给足够时间完成测试) + setTimeout(() => { + console.log('\n⏰ 测试时间到,关闭连接'); + socket.disconnect(); + }, 20000); + + } catch (error) { + console.error('\n❌ 测试失败:', error.message); process.exit(1); - }); - - // 20秒后自动关闭(给足够时间完成测试) - setTimeout(() => { - console.log('⏰ 测试时间到,关闭连接'); - socket.disconnect(); - }, 20000); + } } -console.log('🔧 准备测试环境...'); -testWithUserApiKey().catch(console.error); \ No newline at end of file +// 运行测试 +testWithUserApiKey(); \ No newline at end of file diff --git a/src/business/auth/services/login.service.ts b/src/business/auth/services/login.service.ts index 0fde21b..a5f0072 100644 --- a/src/business/auth/services/login.service.ts +++ b/src/business/auth/services/login.service.ts @@ -663,35 +663,34 @@ export class LoginService { throw new Error('JWT_SECRET未配置'); } - // 1. 创建访问令牌载荷 - const accessPayload: JwtPayload = { + // 1. 创建访问令牌载荷(不包含iss和aud,这些通过options传递) + const accessPayload: Omit = { sub: user.id.toString(), username: user.username, role: user.role, email: user.email, type: 'access', - iat: currentTime, - iss: 'whale-town', - aud: 'whale-town-users', }; // 2. 创建刷新令牌载荷(有效期更长) - const refreshPayload: JwtPayload = { + const refreshPayload: Omit = { sub: user.id.toString(), username: user.username, role: user.role, type: 'refresh', - iat: currentTime, - iss: 'whale-town', - aud: 'whale-town-users', }; - // 3. 生成访问令牌(使用NestJS JwtService) - const accessToken = await this.jwtService.signAsync(accessPayload); + // 3. 生成访问令牌(使用NestJS JwtService,通过options传递iss和aud) + const accessToken = await this.jwtService.signAsync(accessPayload, { + issuer: 'whale-town', + audience: 'whale-town-users', + }); // 4. 生成刷新令牌(有效期30天) const refreshToken = jwt.sign(refreshPayload, jwtSecret, { expiresIn: '30d', + issuer: 'whale-town', + audience: 'whale-town-users', }); // 5. 计算过期时间(秒) diff --git a/src/business/zulip/zulip.module.ts b/src/business/zulip/zulip.module.ts index 13590aa..5fcc0bc 100644 --- a/src/business/zulip/zulip.module.ts +++ b/src/business/zulip/zulip.module.ts @@ -38,8 +38,8 @@ * - 业务规则驱动的消息过滤和权限控制 * * @author angjustinl - * @version 2.0.0 - * @since 2025-12-31 + * @version 1.1.0 + * @since 2026-01-06 */ import { Module } from '@nestjs/common'; @@ -53,6 +53,7 @@ import { ZulipCoreModule } from '../../core/zulip/zulip-core.module'; import { RedisModule } from '../../core/redis/redis.module'; import { LoggerModule } from '../../core/utils/logger/logger.module'; import { LoginCoreModule } from '../../core/login_core/login_core.module'; +import { AuthModule } from '../auth/auth.module'; @Module({ imports: [ @@ -64,6 +65,8 @@ import { LoginCoreModule } from '../../core/login_core/login_core.module'; LoggerModule, // 登录模块 - 提供用户认证和Token验证 LoginCoreModule, + // 认证模块 - 提供JWT验证和用户认证服务 + AuthModule, ], providers: [ // 主协调服务 - 整合各子服务,提供统一业务接口 diff --git a/src/business/zulip/zulip.service.ts b/src/business/zulip/zulip.service.ts index 1386948..2449f65 100644 --- a/src/business/zulip/zulip.service.ts +++ b/src/business/zulip/zulip.service.ts @@ -18,8 +18,8 @@ * - 消息格式转换和过滤 * * @author angjustinl - * @version 1.0.0 - * @since 2025-12-25 + * @version 1.1.0 + * @since 2026-01-06 */ import { Injectable, Logger, Inject } from '@nestjs/common'; @@ -32,6 +32,7 @@ import { IZulipConfigService, } from '../../core/zulip/interfaces/zulip-core.interfaces'; import { ApiKeySecurityService } from '../../core/zulip/services/api_key_security.service'; +import { LoginService } from '../auth/services/login.service'; /** * 玩家登录请求接口 @@ -116,6 +117,7 @@ export class ZulipService { @Inject('ZULIP_CONFIG_SERVICE') private readonly configManager: IZulipConfigService, private readonly apiKeySecurityService: ApiKeySecurityService, + private readonly loginService: LoginService, ) { this.logger.log('ZulipService初始化完成'); } @@ -172,9 +174,7 @@ export class ZulipService { }; } - // 2. 验证游戏Token并获取用户信息 - // TODO: 实际项目中应该调用认证服务验证Token - // 这里暂时使用模拟数据 + // 2. 验证游戏Token并获取用户信息 调用认证服务验证Token const userInfo = await this.validateGameToken(request.token); if (!userInfo) { this.logger.warn('登录失败:Token验证失败', { @@ -288,7 +288,7 @@ export class ZulipService { * 功能描述: * 验证游戏Token的有效性,返回用户信息 * - * @param token 游戏Token + * @param token 游戏Token (JWT) * @returns Promise 用户信息,验证失败返回null * @private */ @@ -299,69 +299,84 @@ export class ZulipService { zulipEmail?: string; zulipApiKey?: string; } | null> { - // TODO: 实际项目中应该调用认证服务验证Token (登录godot所获取的JWT token) - // 这里暂时使用模拟数据进行开发测试 - this.logger.debug('验证游戏Token', { operation: 'validateGameToken', tokenLength: token.length, }); - // 模拟Token验证 - // 实际实现应该: - // 1. 调用LoginService验证Token - // 2. 从数据库获取用户的Zulip API Key - // 3. 返回完整的用户信息 - - if (token.startsWith('invalid')) { - return null; - } - - // 从Token中提取用户ID(模拟) - const userId = `user_${token.substring(0, 8)}`; - - // 从ApiKeySecurityService获取真实的Zulip API Key - let zulipApiKey = undefined; - let zulipEmail = undefined; - try { - // 尝试从Redis获取存储的API Key - const apiKeyResult = await this.apiKeySecurityService.getApiKey(userId); - - if (apiKeyResult.success && apiKeyResult.apiKey) { - zulipApiKey = apiKeyResult.apiKey; - // TODO: 从数据库获取用户的Zulip邮箱 - // 暂时使用模拟数据 - zulipEmail = 'angjustinl@163.com'; - - this.logger.log('从存储获取到Zulip API Key', { + // 1. 使用LoginService验证JWT token + const payload = await this.loginService.verifyToken(token, 'access'); + + if (!payload || !payload.sub) { + this.logger.warn('Token载荷无效', { operation: 'validateGameToken', - userId, - hasApiKey: true, - zulipEmail, - }); - } else { - this.logger.debug('用户没有存储的Zulip API Key', { - operation: 'validateGameToken', - userId, }); + return null; } - } catch (error) { - const err = error as Error; - this.logger.warn('获取Zulip API Key失败', { + + const userId = payload.sub; + const username = payload.username || `user_${userId}`; + const email = payload.email || `${userId}@example.com`; + + this.logger.debug('Token解析成功', { operation: 'validateGameToken', userId, + username, + email, + }); + + // 2. 从ApiKeySecurityService获取真实的Zulip API Key + let zulipApiKey = undefined; + let zulipEmail = undefined; + + try { + // 尝试从Redis获取存储的API Key + const apiKeyResult = await this.apiKeySecurityService.getApiKey(userId); + + if (apiKeyResult.success && apiKeyResult.apiKey) { + zulipApiKey = apiKeyResult.apiKey; + // 使用游戏账号的邮箱 + zulipEmail = email; + + this.logger.log('从存储获取到Zulip API Key', { + operation: 'validateGameToken', + userId, + hasApiKey: true, + apiKeyLength: zulipApiKey.length, + }); + } else { + this.logger.debug('用户没有存储的Zulip API Key', { + operation: 'validateGameToken', + userId, + reason: apiKeyResult.message, + }); + } + } catch (error) { + const err = error as Error; + this.logger.warn('获取Zulip API Key失败', { + operation: 'validateGameToken', + userId, + error: err.message, + }); + } + + return { + userId, + username, + email, + zulipEmail, + zulipApiKey, + }; + + } catch (error) { + const err = error as Error; + this.logger.warn('Token验证失败', { + operation: 'validateGameToken', error: err.message, }); + return null; } - - return { - userId, - username: `Player_${userId.substring(5, 10)}`, - email: `${userId}@example.com`, - zulipEmail, - zulipApiKey, - }; } /**