refactor: 更新WebSocket相关测试和location_broadcast模块

- 更新location_broadcast网关以支持原生WebSocket
- 修改WebSocket认证守卫和中间件
- 更新相关的测试文件和规范
- 添加WebSocket测试工具
- 完善Zulip服务的测试覆盖

技术改进:
- 统一WebSocket实现架构
- 优化性能监控和限流中间件
- 更新测试用例以适配新的WebSocket实现
This commit is contained in:
moyin
2026-01-09 17:02:43 +08:00
parent e9dc887c59
commit cbf4120ddd
13 changed files with 752 additions and 524 deletions

View File

@@ -20,34 +20,41 @@
* - 提供错误处理和日志记录
*
* 最近修改:
* - 2026-01-08: 代码重构 - 拆分长方法,提高代码可读性和可维护性 (修改者: moyin)
* - 2026-01-09: 重构为原生WebSocket - 适配原生WebSocket接口 (修改者: moyin)
*
* @author moyin
* @version 1.1.0
* @version 2.0.0
* @since 2026-01-08
* @lastModified 2026-01-08
* @lastModified 2026-01-09
*/
import { Injectable, CanActivate, ExecutionContext, Logger } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';
import { LoginCoreService, JwtPayload } from '../../core/login_core/login_core.service';
/**
* 扩展的WebSocket客户端接口包含用户信息
*
* 职责:
* - 扩展Socket.io的Socket接口
* - 扩展原生WebSocket接口
* - 添加用户认证信息到客户端对象
* - 提供类型安全的用户数据访问
*/
export interface AuthenticatedSocket extends Socket {
export interface AuthenticatedSocket extends WebSocket {
/** 客户端ID */
id: string;
/** 认证用户信息 */
user: JwtPayload;
user?: JwtPayload;
/** 用户ID便于快速访问 */
userId: string;
userId?: string;
/** 认证时间戳 */
authenticatedAt: number;
authenticatedAt?: number;
/** 会话ID集合 */
sessionIds?: Set<string>;
/** 连接超时 */
connectionTimeout?: NodeJS.Timeout;
/** 心跳状态 */
isAlive?: boolean;
}
@Injectable()
@@ -71,19 +78,9 @@ export class WebSocketAuthGuard implements CanActivate {
* @param context 执行上下文包含WebSocket客户端信息
* @returns Promise<boolean> 认证是否成功
* @throws WsException 当令牌缺失或无效时
*
* @example
* ```typescript
* @SubscribeMessage('join_session')
* @UseGuards(WebSocketAuthGuard)
* handleJoinSession(@ConnectedSocket() client: AuthenticatedSocket) {
* // 此方法需要有效的JWT令牌才能访问
* console.log('认证用户:', client.user.username);
* }
* ```
*/
async canActivate(context: ExecutionContext): Promise<boolean> {
const client = context.switchToWs().getClient<Socket>();
const client = context.switchToWs().getClient<AuthenticatedSocket>();
const data = context.switchToWs().getData();
this.logAuthStart(client, context);
@@ -95,6 +92,15 @@ export class WebSocketAuthGuard implements CanActivate {
this.handleMissingToken(client);
}
// 如果是缓存的认证信息,直接返回成功
if (token === 'cached' && client.user && client.userId) {
this.logger.debug('使用缓存的认证信息', {
socketId: client.id,
userId: client.userId,
});
return true;
}
const payload = await this.loginCoreService.verifyToken(token, 'access');
this.attachUserToClient(client, payload);
this.logAuthSuccess(client, payload);
@@ -113,7 +119,7 @@ export class WebSocketAuthGuard implements CanActivate {
* @param context 执行上下文
* @private
*/
private logAuthStart(client: Socket, context: ExecutionContext): void {
private logAuthStart(client: AuthenticatedSocket, context: ExecutionContext): void {
this.logger.log('开始WebSocket认证验证', {
operation: 'websocket_auth',
socketId: client.id,
@@ -129,7 +135,7 @@ export class WebSocketAuthGuard implements CanActivate {
* @throws WsException
* @private
*/
private handleMissingToken(client: Socket): never {
private handleMissingToken(client: AuthenticatedSocket): never {
this.logger.warn('WebSocket认证失败缺少认证令牌', {
operation: 'websocket_auth',
socketId: client.id,
@@ -151,11 +157,10 @@ export class WebSocketAuthGuard implements CanActivate {
* @param payload JWT载荷
* @private
*/
private attachUserToClient(client: Socket, payload: JwtPayload): void {
const authenticatedClient = client as AuthenticatedSocket;
authenticatedClient.user = payload;
authenticatedClient.userId = payload.sub;
authenticatedClient.authenticatedAt = Date.now();
private attachUserToClient(client: AuthenticatedSocket, payload: JwtPayload): void {
client.user = payload;
client.userId = payload.sub;
client.authenticatedAt = Date.now();
}
/**
@@ -165,7 +170,7 @@ export class WebSocketAuthGuard implements CanActivate {
* @param payload JWT载荷
* @private
*/
private logAuthSuccess(client: Socket, payload: JwtPayload): void {
private logAuthSuccess(client: AuthenticatedSocket, payload: JwtPayload): void {
this.logger.log('WebSocket认证成功', {
operation: 'websocket_auth',
socketId: client.id,
@@ -184,7 +189,7 @@ export class WebSocketAuthGuard implements CanActivate {
* @throws WsException
* @private
*/
private handleAuthError(client: Socket, error: any): never {
private handleAuthError(client: AuthenticatedSocket, error: any): never {
this.logger.error('WebSocket认证失败', {
operation: 'websocket_auth',
socketId: client.id,
@@ -214,43 +219,18 @@ export class WebSocketAuthGuard implements CanActivate {
*
* 技术实现:
* 1. 优先从消息数据中提取token字段
* 2. 从连接握手的查询参数中提取token
* 3. 从连接握手的认证头中提取Bearer令牌
* 4. 从Socket客户端的自定义属性中提取
* 2. 检查是否已经认证过(用于后续消息)
* 3. 从URL查询参数中提取token如果可用
*
* 支持的令牌传递方式:
* - 消息数据: { token: "jwt_token" }
* - 查询参数: ?token=jwt_token
* - 认证头: Authorization: Bearer jwt_token
* - Socket属性: client.handshake.auth.token
* - 缓存认证: 使用已验证的用户信息
*
* @param client WebSocket客户端对象
* @param data 消息数据
* @returns JWT令牌字符串或undefined
*
* @example
* ```typescript
* // 方式1: 在消息中传递token
* socket.emit('join_session', {
* type: 'join_session',
* sessionId: 'session123',
* token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
* });
*
* // 方式2: 在连接时传递token
* const socket = io('ws://localhost:3000', {
* query: { token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' }
* });
*
* // 方式3: 在认证头中传递token
* const socket = io('ws://localhost:3000', {
* extraHeaders: {
* 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
* }
* });
* ```
*/
private extractToken(client: Socket, data: any): string | undefined {
private extractToken(client: AuthenticatedSocket, data: any): string | undefined {
// 1. 优先从消息数据中提取token
if (data && typeof data === 'object' && data.token) {
this.logger.debug('从消息数据中提取到token', {
@@ -260,45 +240,11 @@ export class WebSocketAuthGuard implements CanActivate {
return data.token;
}
// 2. 从查询参数中提取token
const queryToken = client.handshake.query?.token;
if (queryToken && typeof queryToken === 'string') {
this.logger.debug('从查询参数中提取到token', {
socketId: client.id,
source: 'query_params'
});
return queryToken;
}
// 3. 从认证头中提取Bearer令牌
const authHeader = client.handshake.headers?.authorization;
if (authHeader && typeof authHeader === 'string') {
const [type, token] = authHeader.split(' ');
if (type === 'Bearer' && token) {
this.logger.debug('从认证头中提取到token', {
socketId: client.id,
source: 'auth_header'
});
return token;
}
}
// 4. 从Socket认证对象中提取token
const authToken = client.handshake.auth?.token;
if (authToken && typeof authToken === 'string') {
this.logger.debug('从Socket认证对象中提取到token', {
socketId: client.id,
source: 'socket_auth'
});
return authToken;
}
// 5. 检查是否已经认证过(用于后续消息)
const authenticatedClient = client as AuthenticatedSocket;
if (authenticatedClient.user && authenticatedClient.userId) {
// 2. 检查是否已经认证过(用于后续消息)
if (client.user && client.userId) {
this.logger.debug('使用已认证的用户信息', {
socketId: client.id,
userId: authenticatedClient.userId,
userId: client.userId,
source: 'cached_auth'
});
return 'cached'; // 返回特殊标识,表示使用缓存的认证信息
@@ -308,9 +254,7 @@ export class WebSocketAuthGuard implements CanActivate {
socketId: client.id,
availableSources: {
messageData: !!data?.token,
queryParams: !!client.handshake.query?.token,
authHeader: !!client.handshake.headers?.authorization,
socketAuth: !!client.handshake.auth?.token
cachedAuth: !!(client.user && client.userId)
}
});
@@ -322,10 +266,9 @@ export class WebSocketAuthGuard implements CanActivate {
*
* @param client WebSocket客户端
*/
static clearAuthentication(client: Socket): void {
const authenticatedClient = client as AuthenticatedSocket;
delete authenticatedClient.user;
delete authenticatedClient.userId;
delete authenticatedClient.authenticatedAt;
static clearAuthentication(client: AuthenticatedSocket): void {
delete client.user;
delete client.userId;
delete client.authenticatedAt;
}
}