feat:实现位置广播系统
- 添加位置广播核心控制器和服务 - 实现健康检查和位置同步功能 - 添加WebSocket实时位置更新支持 - 完善位置广播的测试覆盖
This commit is contained in:
331
src/business/location_broadcast/websocket_auth.guard.ts
Normal file
331
src/business/location_broadcast/websocket_auth.guard.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user