WebSocket 守卫与常规 HTTP 应用守卫没有本质区别。唯一的不同在于,你应该使用 WsException
而不是抛出 HttpException
。
info 提示
WsException
类是从@nestjs/websockets
包中导出的。
以下示例使用了方法作用域的守卫。与基于 HTTP 的应用一样,你也可以使用网关作用域的守卫(即在网关类前添加 @UseGuards()
装饰器)。
@UseGuards(AuthGuard)
@SubscribeMessage('events')
handleEvent(client: Client, data: unknown): WsResponse<unknown> {
const event = 'events';
return { event, data };
}
if (!authToken) {
throw new WsException('未找到认证令牌');
}
const payload = await this.jwtService.verifyAsync(authToken);
// 将用户信息添加到 socket 中
client.data.user = payload;
return true;
} catch (error) {
throw new WsException('认证失败');
}
}
private extractTokenFromSocket(client: Socket): string | undefined { const authHeader = client.handshake.headers.authorization; if (authHeader && authHeader.startsWith('Bearer ')) { return authHeader.substring(7); }
// 也可以从查询参数中获取令牌
return client.handshake.query.token as string;
} }
#### 角色守卫
实现基于角色的访问控制:
```typescript
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';
@Injectable()
export class WsRolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
if (!requiredRoles) {
return true;
}
const client: Socket = context.switchToWs().getClient<Socket>();
const user = client.data.user;
if (!user) {
throw new WsException('用户未认证');
}
const hasRole = requiredRoles.some(role => user.roles?.includes(role));
if (!hasRole) {
throw new WsException('权限不足');
}
return true;
}
}
// 角色装饰器
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
// 使用示例
@UseGuards(WsJwtGuard, WsRolesGuard)
@Roles('admin', 'moderator')
@SubscribeMessage('admin-message')
handleAdminMessage(@MessageBody() data: any) {
return { message: '管理员消息已处理' };
}
防止消息洪水攻击:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';
@Injectable()
export class WsRateLimitGuard implements CanActivate {
private readonly clients = new Map<string, { count: number; resetTime: number }>();
private readonly maxRequests = 10; // 每分钟最大请求数
private readonly windowMs = 60 * 1000; // 1 分钟窗口
canActivate(context: ExecutionContext): boolean {
const client: Socket = context.switchToWs().getClient<Socket>();
const clientId = client.id;
const now = Date.now();
if (!this.clients.has(clientId)) {
this.clients.set(clientId, { count: 1, resetTime: now + this.windowMs });
return true;
}
const clientData = this.clients.get(clientId);
if (now > clientData.resetTime) {
// 重置计数器
clientData.count = 1;
clientData.resetTime = now + this.windowMs;
return true;
}
if (clientData.count >= this.maxRequests) {
throw new WsException('请求过于频繁,请稍后再试');
}
clientData.count++;
return true;
}
}
检查用户是否有权限访问特定房间:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';
@Injectable()
export class WsRoomGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const client: Socket = context.switchToWs().getClient<Socket>();
const data = context.switchToWs().getData();
const user = client.data.user;
const roomId = data.roomId || data.room;
if (!user) {
throw new WsException('用户未认证');
}
if (!roomId) {
throw new WsException('房间ID未指定');
}
// 检查用户是否有权限访问房间
const hasAccess = await this.checkRoomAccess(user.id, roomId);
if (!hasAccess) {
throw new WsException('无权限访问该房间');
}
return true;
}
private async checkRoomAccess(userId: string, roomId: string): Promise<boolean> {
// 实现房间访问权限逻辑
// 例如:检查数据库中的用户-房间关系
return true; // 简化示例
}
}
在连接建立时进行认证:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class WsConnectionGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const client: Socket = context.switchToWs().getClient<Socket>();
try {
const token = client.handshake.query.token as string;
if (!token) {
client.disconnect();
return false;
}
const payload = await this.jwtService.verifyAsync(token);
client.data.user = payload;
return true;
} catch (error) {
client.disconnect();
return false;
}
}
}
创建一个组合多个检查的守卫:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';
@Injectable()
export class WsCompositeGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const client: Socket = context.switchToWs().getClient<Socket>();
// 1. 检查用户认证
if (!client.data.user) {
throw new WsException('用户未认证');
}
// 2. 检查用户状态
if (client.data.user.status !== 'active') {
throw new WsException('用户账户已被禁用');
}
// 3. 检查连接数限制
const userConnections = await this.getUserConnectionCount(client.data.user.id);
if (userConnections >= 5) {
throw new WsException('连接数超过限制');
}
// 4. 检查 IP 白名单(如果需要)
const clientIp = client.handshake.address;
if (!this.isIpAllowed(clientIp)) {
throw new WsException('IP 地址不在允许列表中');
}
return true;
}
private async getUserConnectionCount(userId: string): Promise<number> {
// 实现获取用户连接数的逻辑
return 1; // 简化示例
}
private isIpAllowed(ip: string): boolean {
// 实现 IP 白名单检查
return true; // 简化示例
}
}
设置全局 WebSocket 守卫:
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { WsJwtGuard } from './guards/ws-jwt.guard';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: WsJwtGuard,
},
],
})
export class AppModule {}
完整的使用示例:
@WebSocketGateway()
@UseGuards(WsJwtGuard) // 网关级别的守卫
export class SecureGateway {
@UseGuards(WsRateLimitGuard)
@SubscribeMessage('public-message')
handlePublicMessage(@MessageBody() data: any) {
return { message: '公共消息已处理' };
}
@UseGuards(WsRolesGuard, WsRoomGuard)
@Roles('admin')
@SubscribeMessage('admin-room-message')
handleAdminRoomMessage(@MessageBody() data: any) {
return { message: '管理员房间消息已处理' };
}
@UseGuards(WsCompositeGuard)
@SubscribeMessage('secure-operation')
handleSecureOperation(@MessageBody() data: any) {
return { message: '安全操作已执行' };
}
}
通过这些守卫,你可以为 WebSocket 应用实现完整的安全控制,包括认证、授权、速率限制和访问控制。