refactor: 更新WebSocket相关测试和location_broadcast模块
- 更新location_broadcast网关以支持原生WebSocket - 修改WebSocket认证守卫和中间件 - 更新相关的测试文件和规范 - 添加WebSocket测试工具 - 完善Zulip服务的测试覆盖 技术改进: - 统一WebSocket实现架构 - 优化性能监控和限流中间件 - 更新测试用例以适配新的WebSocket实现
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user