/** * WebSocket 认证守卫 * 用于验证 Socket.IO 连接的用户身份 */ import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import type { Socket } from 'socket.io'; import { WsException } from '@nestjs/websockets'; import { RedisDatabase, RedisService } from '../../plugins/redis/redis.service'; import { LoggerService } from '../../plugins/logger/logger.service'; import { MeetingService } from '../meeting/meeting.service'; import type { MeetingWsUser } from './types'; @Injectable() export class MeetingAuthGuard implements CanActivate { public constructor( private readonly redisService: RedisService, private readonly logger: LoggerService, private readonly meetingService: MeetingService ) {} public async canActivate(context: ExecutionContext): Promise { // 获取 Socket 实例 const socket = context.switchToWs().getClient(); // 检查是否已经通过连接认证 if (socket.data.user) { return true; } try { const user = await this.validateToken(socket); socket.data.user = user; return true; } catch (e) { if (e instanceof WsException) { this.logger.error(e.message, 'WsAuthGuard'); throw e; } this.logger.error(e, 'WsAuthGuard'); throw new WsException('WebSocket 鉴权失败'); } } /** * 验证 Token 并返回用户信息 * 可供 handleConnection 直接调用 */ public async validateToken(socket: Socket): Promise { // 尝试从握手信息中获取认证头(兼容大小写) const handshake = socket.handshake; const authHeader = String( handshake.auth?.authorization || handshake.auth?.Authorization || handshake.headers?.authorization || handshake.headers?.Authorization || '' ); const customMac = String( handshake.auth?.['custom-mac'] || handshake.auth?.['Custom-Mac'] || handshake.headers?.['custom-mac'] || handshake.headers?.['Custom-Mac'] || '' ); const customPlatform = String( handshake.auth?.['custom-platform'] || handshake.auth?.['Custom-Platform'] || handshake.headers?.['custom-platform'] || handshake.headers?.['Custom-Platform'] || '' ); const customTimestamp = String( handshake.auth?.['custom-timestamp'] || handshake.auth?.['Custom-Timestamp'] || handshake.headers?.['custom-timestamp'] || handshake.headers?.['Custom-Timestamp'] || '' ); // 验证必要的认证信息 if (!authHeader || !authHeader.toLowerCase().startsWith('bearer')) { throw new WsException('缺少或无效的 Authorization'); } if (!customMac) { throw new WsException('缺少 Custom-Mac'); } if (!customPlatform) { throw new WsException('缺少 Custom-Platform'); } if (!customTimestamp) { throw new WsException('缺少 Custom-Timestamp'); } // 解析 Token const token = authHeader.slice(7).trim(); const parts = token.split('.'); if (parts.length < 2) { throw new WsException('无效的 Token 格式'); } const payload: any = (() => { try { const json = Buffer.from(parts[1], 'base64url').toString('utf8'); return JSON.parse(json); } catch { throw new WsException('无法解析 Token'); } })(); const NAME_ID_CLAIM = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'; const ROLE_CLAIM = 'http://schemas.microsoft.com/ws/2008/06/identity/claims/role'; const userId = String(payload?.[NAME_ID_CLAIM] || ''); const role = Number(payload?.[ROLE_CLAIM] || '-1'); if (!userId || isNaN(role) || role < 0) { throw new WsException('Token 非法'); } // 验证 Token 过期时间 const nowSec = Math.floor(Date.now() / 1000); const exp = Number(payload?.exp || 0); if (!Number.isFinite(exp) || nowSec >= exp) { throw new WsException('Token 已过期'); } const platform = customPlatform.toUpperCase(); // 验证 Redis 中的登录态 const redisKey = `Auth:${userId}:${platform}`; const redis = this.redisService.getClient(RedisDatabase.GLOBAL); const v = await redis.get(redisKey); if (!v) { throw new WsException('未找到登录态'); } const redisObj: any = (() => { try { return JSON.parse(v); } catch { throw new WsException('登录态数据异常'); } })(); const redisToken = redisObj?.Token || redisObj?.token || ''; if (redisToken !== token) { throw new WsException('登录态已失效'); } // 查询数据库获取短 UID 和用户名 const shortUid = await this.meetingService.getOrAssignShortUid(Number(userId)); // 从 Redis 登录态中获取用户名 const userName = redisObj?.userName || redisObj?.UserName || `用户-${userId}`; console.log(`[WsAuthGuard] 用户 ${userId} 获得短 UID: ${shortUid}, userName: ${userName}`); return { userId, role, shortUid, platform, userName, }; } }