feat:实现位置广播系统

- 添加位置广播核心控制器和服务
- 实现健康检查和位置同步功能
- 添加WebSocket实时位置更新支持
- 完善位置广播的测试覆盖
This commit is contained in:
moyin
2026-01-08 23:05:52 +08:00
parent 6924416bbd
commit c31cbe559d
27 changed files with 12212 additions and 0 deletions

View File

@@ -0,0 +1,331 @@
/**
* WebSocket认证守卫
*
* 功能描述:
* - 验证WebSocket连接中的JWT令牌
* - 提取用户信息并添加到WebSocket客户端上下文
* - 保护需要认证的WebSocket事件处理器
* - 处理WebSocket特有的认证流程
*
* 职责分离:
* - 专注于WebSocket环境下的JWT令牌验证
* - 提供统一的WebSocket认证守卫机制
* - 处理WebSocket认证失败的异常情况
* - 支持实时通信的安全认证
*
* 技术实现:
* - 从WebSocket消息中提取JWT令牌
* - 使用现有的LoginCore服务进行令牌验证
* - 将用户信息附加到WebSocket客户端对象
* - 提供错误处理和日志记录
*
* 最近修改:
* - 2026-01-08: 代码重构 - 拆分长方法,提高代码可读性和可维护性 (修改者: moyin)
*
* @author moyin
* @version 1.1.0
* @since 2026-01-08
* @lastModified 2026-01-08
*/
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接口
* - 添加用户认证信息到客户端对象
* - 提供类型安全的用户数据访问
*/
export interface AuthenticatedSocket extends Socket {
/** 认证用户信息 */
user: JwtPayload;
/** 用户ID便于快速访问 */
userId: string;
/** 认证时间戳 */
authenticatedAt: number;
}
@Injectable()
export class WebSocketAuthGuard implements CanActivate {
private readonly logger = new Logger(WebSocketAuthGuard.name);
constructor(private readonly loginCoreService: LoginCoreService) {}
/**
* WebSocket JWT令牌验证和用户认证
*
* 技术实现:
* 1. 从WebSocket客户端获取认证信息
* 2. 提取JWT令牌支持多种提取方式
* 3. 验证令牌的有效性和签名
* 4. 解码令牌获取用户信息
* 5. 将用户信息添加到Socket客户端对象
* 6. 记录认证成功或失败的日志
* 7. 返回认证结果或抛出WebSocket异常
*
* @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 data = context.switchToWs().getData();
this.logAuthStart(client, context);
try {
const token = this.extractToken(client, data);
if (!token) {
this.handleMissingToken(client);
}
const payload = await this.loginCoreService.verifyToken(token, 'access');
this.attachUserToClient(client, payload);
this.logAuthSuccess(client, payload);
return true;
} catch (error) {
this.handleAuthError(client, error);
}
}
/**
* 记录认证开始日志
*
* @param client WebSocket客户端
* @param context 执行上下文
* @private
*/
private logAuthStart(client: Socket, context: ExecutionContext): void {
this.logger.log('开始WebSocket认证验证', {
operation: 'websocket_auth',
socketId: client.id,
eventName: context.getHandler().name,
timestamp: new Date().toISOString()
});
}
/**
* 处理缺少令牌的情况
*
* @param client WebSocket客户端
* @throws WsException
* @private
*/
private handleMissingToken(client: Socket): never {
this.logger.warn('WebSocket认证失败缺少认证令牌', {
operation: 'websocket_auth',
socketId: client.id,
reason: 'missing_token'
});
throw new WsException({
type: 'error',
code: 'INVALID_TOKEN',
message: '缺少认证令牌',
timestamp: Date.now()
});
}
/**
* 将用户信息附加到客户端
*
* @param client WebSocket客户端
* @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();
}
/**
* 记录认证成功日志
*
* @param client WebSocket客户端
* @param payload JWT载荷
* @private
*/
private logAuthSuccess(client: Socket, payload: JwtPayload): void {
this.logger.log('WebSocket认证成功', {
operation: 'websocket_auth',
socketId: client.id,
userId: payload.sub,
username: payload.username,
role: payload.role,
timestamp: new Date().toISOString()
});
}
/**
* 处理认证错误
*
* @param client WebSocket客户端
* @param error 错误对象
* @throws WsException
* @private
*/
private handleAuthError(client: Socket, error: any): never {
this.logger.error('WebSocket认证失败', {
operation: 'websocket_auth',
socketId: client.id,
error: error instanceof Error ? error.message : String(error),
timestamp: new Date().toISOString()
}, error instanceof Error ? error.stack : undefined);
// 如果已经是WsException直接抛出
if (error instanceof WsException) {
throw error;
}
// 转换为WebSocket异常
throw new WsException({
type: 'error',
code: 'INVALID_TOKEN',
message: '无效的认证令牌',
details: {
reason: error instanceof Error ? error.message : String(error)
},
timestamp: Date.now()
});
}
/**
* 从WebSocket连接中提取JWT令牌
*
* 技术实现:
* 1. 优先从消息数据中提取token字段
* 2. 从连接握手的查询参数中提取token
* 3. 从连接握手的认证头中提取Bearer令牌
* 4. 从Socket客户端的自定义属性中提取
*
* 支持的令牌传递方式:
* - 消息数据: { 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 {
// 1. 优先从消息数据中提取token
if (data && typeof data === 'object' && data.token) {
this.logger.debug('从消息数据中提取到token', {
socketId: client.id,
source: 'message_data'
});
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) {
this.logger.debug('使用已认证的用户信息', {
socketId: client.id,
userId: authenticatedClient.userId,
source: 'cached_auth'
});
return 'cached'; // 返回特殊标识,表示使用缓存的认证信息
}
this.logger.warn('未找到有效的认证令牌', {
socketId: client.id,
availableSources: {
messageData: !!data?.token,
queryParams: !!client.handshake.query?.token,
authHeader: !!client.handshake.headers?.authorization,
socketAuth: !!client.handshake.auth?.token
}
});
return undefined;
}
/**
* 清理客户端的认证信息
*
* @param client WebSocket客户端
*/
static clearAuthentication(client: Socket): void {
const authenticatedClient = client as AuthenticatedSocket;
delete authenticatedClient.user;
delete authenticatedClient.userId;
delete authenticatedClient.authenticatedAt;
}
}