forked from datawhale/whale-town-end
Merge branch 'main' into zulip_dev
This commit is contained in:
39
src/business/auth/decorators/current-user.decorator.ts
Normal file
39
src/business/auth/decorators/current-user.decorator.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 当前用户装饰器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 从请求上下文中提取当前认证用户信息
|
||||
* - 简化控制器中获取用户信息的操作
|
||||
*
|
||||
* 使用示例:
|
||||
* ```typescript
|
||||
* @Get('profile')
|
||||
* @UseGuards(JwtAuthGuard)
|
||||
* getProfile(@CurrentUser() user: JwtPayload) {
|
||||
* return { user };
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-01-05
|
||||
*/
|
||||
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import { AuthenticatedRequest, JwtPayload } from '../guards/jwt-auth.guard';
|
||||
|
||||
/**
|
||||
* 当前用户装饰器
|
||||
*
|
||||
* @param data 可选的属性名,用于获取用户对象的特定属性
|
||||
* @param ctx 执行上下文
|
||||
* @returns 用户信息或用户的特定属性
|
||||
*/
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(data: keyof JwtPayload | undefined, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest<AuthenticatedRequest>();
|
||||
const user = request.user;
|
||||
|
||||
return data ? user?.[data] : user;
|
||||
},
|
||||
);
|
||||
128
src/business/auth/examples/jwt-usage-example.ts
Normal file
128
src/business/auth/examples/jwt-usage-example.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* JWT 使用示例
|
||||
*
|
||||
* 展示如何在控制器中使用 JWT 认证守卫和当前用户装饰器
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-01-05
|
||||
*/
|
||||
|
||||
import { Controller, Get, UseGuards, Post, Body } from '@nestjs/common';
|
||||
import { JwtAuthGuard, JwtPayload } from '../guards/jwt-auth.guard';
|
||||
import { CurrentUser } from '../decorators/current-user.decorator';
|
||||
|
||||
/**
|
||||
* 示例控制器 - 展示 JWT 认证的使用方法
|
||||
*/
|
||||
@Controller('example')
|
||||
export class ExampleController {
|
||||
|
||||
/**
|
||||
* 公开接口 - 无需认证
|
||||
*/
|
||||
@Get('public')
|
||||
getPublicData() {
|
||||
return {
|
||||
message: '这是一个公开接口,无需认证',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 受保护的接口 - 需要 JWT 认证
|
||||
*
|
||||
* 请求头示例:
|
||||
* Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
*/
|
||||
@Get('protected')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
getProtectedData(@CurrentUser() user: JwtPayload) {
|
||||
return {
|
||||
message: '这是一个受保护的接口,需要有效的 JWT 令牌',
|
||||
user: {
|
||||
id: user.sub,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
@Get('profile')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
getUserProfile(@CurrentUser() user: JwtPayload) {
|
||||
return {
|
||||
profile: {
|
||||
userId: user.sub,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
tokenIssuedAt: new Date(user.iat * 1000).toISOString(),
|
||||
tokenExpiresAt: new Date(user.exp * 1000).toISOString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的特定属性
|
||||
*/
|
||||
@Get('username')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
getUsername(@CurrentUser('username') username: string) {
|
||||
return {
|
||||
username,
|
||||
message: `你好,${username}!`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 需要特定角色的接口
|
||||
*/
|
||||
@Post('admin-only')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
adminOnlyAction(@CurrentUser() user: JwtPayload, @Body() data: any) {
|
||||
// 检查用户角色
|
||||
if (user.role !== 1) { // 假设 1 是管理员角色
|
||||
return {
|
||||
success: false,
|
||||
message: '权限不足,仅管理员可访问',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '管理员操作执行成功',
|
||||
data,
|
||||
operator: user.username,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用说明:
|
||||
*
|
||||
* 1. 首先调用登录接口获取 JWT 令牌:
|
||||
* POST /auth/login
|
||||
* {
|
||||
* "identifier": "username",
|
||||
* "password": "password"
|
||||
* }
|
||||
*
|
||||
* 2. 从响应中获取 access_token
|
||||
*
|
||||
* 3. 在后续请求中添加 Authorization 头:
|
||||
* Authorization: Bearer <access_token>
|
||||
*
|
||||
* 4. 访问受保护的接口:
|
||||
* GET /example/protected
|
||||
* GET /example/profile
|
||||
* GET /example/username
|
||||
* POST /example/admin-only
|
||||
*
|
||||
* 错误处理:
|
||||
* - 401 Unauthorized: 令牌缺失或无效
|
||||
* - 403 Forbidden: 令牌有效但权限不足
|
||||
*/
|
||||
83
src/business/auth/guards/jwt-auth.guard.ts
Normal file
83
src/business/auth/guards/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* JWT 认证守卫
|
||||
*
|
||||
* 功能描述:
|
||||
* - 验证请求中的 JWT 令牌
|
||||
* - 提取用户信息并添加到请求上下文
|
||||
* - 保护需要认证的路由
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-01-05
|
||||
*/
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
UnauthorizedException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Request } from 'express';
|
||||
|
||||
/**
|
||||
* JWT 载荷接口
|
||||
*/
|
||||
export interface JwtPayload {
|
||||
sub: string; // 用户ID
|
||||
username: string;
|
||||
role: number;
|
||||
iat: number; // 签发时间
|
||||
exp: number; // 过期时间
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展的请求接口,包含用户信息
|
||||
*/
|
||||
export interface AuthenticatedRequest extends Request {
|
||||
user: JwtPayload;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard implements CanActivate {
|
||||
private readonly logger = new Logger(JwtAuthGuard.name);
|
||||
|
||||
constructor(private readonly jwtService: JwtService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
|
||||
if (!token) {
|
||||
this.logger.warn('访问被拒绝:缺少认证令牌');
|
||||
throw new UnauthorizedException('缺少认证令牌');
|
||||
}
|
||||
|
||||
try {
|
||||
// 验证并解码 JWT 令牌
|
||||
const payload = await this.jwtService.verifyAsync<JwtPayload>(token);
|
||||
|
||||
// 将用户信息添加到请求对象
|
||||
(request as AuthenticatedRequest).user = payload;
|
||||
|
||||
this.logger.log(`用户认证成功: ${payload.username} (ID: ${payload.sub})`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
this.logger.warn(`JWT 令牌验证失败: ${errorMessage}`);
|
||||
throw new UnauthorizedException('无效的认证令牌');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头中提取 JWT 令牌
|
||||
*
|
||||
* @param request 请求对象
|
||||
* @returns JWT 令牌或 undefined
|
||||
*/
|
||||
private extractTokenFromHeader(request: Request): string | undefined {
|
||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,9 @@ export class ZulipService {
|
||||
private readonly loginService: LoginService,
|
||||
) {
|
||||
this.logger.log('ZulipService初始化完成');
|
||||
|
||||
// 启动事件处理
|
||||
this.initializeEventProcessing();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -775,5 +778,42 @@ export class ZulipService {
|
||||
async getSocketsInMap(mapId: string): Promise<string[]> {
|
||||
return this.sessionManager.getSocketsInMap(mapId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件处理器实例
|
||||
*
|
||||
* 功能描述:
|
||||
* 返回ZulipEventProcessorService实例,用于设置消息分发器
|
||||
*
|
||||
* @returns ZulipEventProcessorService 事件处理器实例
|
||||
*/
|
||||
getEventProcessor(): ZulipEventProcessorService {
|
||||
return this.eventProcessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化事件处理
|
||||
*
|
||||
* 功能描述:
|
||||
* 启动Zulip事件处理循环,用于接收和处理从Zulip服务器返回的消息
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private async initializeEventProcessing(): Promise<void> {
|
||||
try {
|
||||
this.logger.log('开始初始化Zulip事件处理');
|
||||
|
||||
// 启动事件处理循环
|
||||
await this.eventProcessor.startEventProcessing();
|
||||
|
||||
this.logger.log('Zulip事件处理初始化完成');
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.error('初始化Zulip事件处理失败', {
|
||||
operation: 'initializeEventProcessing',
|
||||
error: err.message,
|
||||
}, err.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -139,6 +139,9 @@ export class ZulipWebSocketGateway implements OnGatewayConnection, OnGatewayDisc
|
||||
namespace: '/game',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// 设置消息分发器,使ZulipEventProcessorService能够向客户端发送消息
|
||||
this.setupMessageDistributor();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -373,6 +376,13 @@ export class ZulipWebSocketGateway implements OnGatewayConnection, OnGatewayDisc
|
||||
): Promise<void> {
|
||||
const clientData = client.data as ClientData | undefined;
|
||||
|
||||
console.log('🔍 DEBUG: handleChat 被调用了!', {
|
||||
socketId: client.id,
|
||||
data: data,
|
||||
clientData: clientData,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
this.logger.log('收到聊天消息', {
|
||||
operation: 'handleChat',
|
||||
socketId: client.id,
|
||||
@@ -749,5 +759,41 @@ export class ZulipWebSocketGateway implements OnGatewayConnection, OnGatewayDisc
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置消息分发器
|
||||
*
|
||||
* 功能描述:
|
||||
* 将当前WebSocket网关设置为ZulipEventProcessorService的消息分发器,
|
||||
* 使其能够接收从Zulip返回的消息并转发给游戏客户端
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private setupMessageDistributor(): void {
|
||||
try {
|
||||
// 获取ZulipEventProcessorService实例
|
||||
const eventProcessor = this.zulipService.getEventProcessor();
|
||||
|
||||
if (eventProcessor) {
|
||||
// 设置消息分发器
|
||||
eventProcessor.setMessageDistributor(this);
|
||||
|
||||
this.logger.log('消息分发器设置完成', {
|
||||
operation: 'setupMessageDistributor',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
this.logger.warn('无法获取ZulipEventProcessorService实例', {
|
||||
operation: 'setupMessageDistributor',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.error('设置消息分发器失败', {
|
||||
operation: 'setupMessageDistributor',
|
||||
error: err.message,
|
||||
}, err.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user