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 密钥。
This commit is contained in:
angjustinl
2026-01-06 18:51:37 +08:00
parent 3733717d1f
commit 8f9a6e7f9d
8 changed files with 763 additions and 187 deletions

View File

@@ -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);
// 运行测试
testWithUserApiKey();