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

@@ -663,35 +663,34 @@ export class LoginService {
throw new Error('JWT_SECRET未配置');
}
// 1. 创建访问令牌载荷
const accessPayload: JwtPayload = {
// 1. 创建访问令牌载荷不包含iss和aud这些通过options传递
const accessPayload: Omit<JwtPayload, 'iss' | 'aud' | 'iat' | 'exp'> = {
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<JwtPayload, 'iss' | 'aud' | 'iat' | 'exp'> = {
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. 计算过期时间(秒)

View File

@@ -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: [
// 主协调服务 - 整合各子服务,提供统一业务接口

View File

@@ -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<UserInfo | null> 用户信息验证失败返回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,
};
}
/**