Files
tauri-meeting/node_api/src/modules/websocket/meeting-auth.guard.ts

159 lines
5.1 KiB
TypeScript
Raw Normal View History

/**
* 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<boolean> {
// 获取 Socket 实例
const socket = context.switchToWs().getClient<Socket>();
// 检查是否已经通过连接认证
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<MeetingWsUser> {
// 尝试从握手信息中获取认证头(兼容大小写)
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 || '';
// 因为会有同一个账号同平台登录多个监控端和教师端使用同一个账号也都是web所以不做过多验证
if (!redisToken) {
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,
};
}
}