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:
@@ -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. 计算过期时间(秒)
|
||||
|
||||
@@ -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: [
|
||||
// 主协调服务 - 整合各子服务,提供统一业务接口
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user