0Pricing
NestJS Enterprise Backend APIs · 강의

소켓 연결 인증 및 보호

안전한 실시간 액세스를 위해 핸드셰이크와 메시지 이벤트에 가드 및 토큰 검증을 적용합니다

소켓 연결 인증 및 보호은(는) CoddyKit의 무료 NestJS Enterprise Backend APIs 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 NestJS Enterprise Backend APIs 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. NestJS Enterprise Backend APIs 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

Why Sockets Need Their Own Auth Story

HTTP routes in NestJS are protected by middleware and guards that read the Authorization header on every request. WebSockets are different: the client opens one long-lived connection during the handshake, then exchanges many messages over it.

  • You authenticate once at connection time, not per message.
  • The socket stays open for minutes or hours, so a token that expires mid-session is a real concern.
  • Standard HTTP guards do not automatically run on WebSocket events.

This lesson shows how to verify a token during the handshake, attach the user to the socket, and guard individual message events for secure realtime access.

Where the Token Lives in a Handshake

A browser WebSocket cannot set custom headers, so clients pass the token in one of three places during the Socket.IO handshake:

  • handshake.auth.token — the modern, preferred slot (set via the client auth option).
  • handshake.headers.authorization — works when a native header is available.
  • handshake.query.token — a fallback, but tokens land in server/proxy logs, so avoid it.

A small helper centralizes extraction so every guard and lifecycle hook reads the token the same way.

import { Socket } from 'socket.io';

export function extractToken(client: Socket): string | null {
  const auth = client.handshake.auth?.token;
  if (typeof auth === 'string') return auth;

  const header = client.handshake.headers?.authorization;
  if (typeof header === 'string' && header.startsWith('Bearer ')) {
    return header.slice(7);
  }

  return null;
}

Verifying on Connection with handleConnection

The cleanest place to authenticate is the gateway's handleConnection lifecycle hook. It runs the moment a client connects. If the token is missing or invalid, call client.disconnect() so the socket never participates in any room or event.

On success, attach the decoded user to client.data — a per-socket bag that survives for the connection's lifetime and is readable in every later event handler.

import { OnGatewayConnection, WebSocketGateway } from '@nestjs/websockets';
import { JwtService } from '@nestjs/jwt';
import { Socket } from 'socket.io';
import { extractToken } from './extract-token';

@WebSocketGateway({ cors: true })
export class ChatGateway implements OnGatewayConnection {
  constructor(private readonly jwt: JwtService) {}

  async handleConnection(client: Socket) {
    try {
      const token = extractToken(client);
      if (!token) throw new Error('No token');
      const payload = await this.jwt.verifyAsync(token);
      client.data.user = { id: payload.sub, role: payload.role };
    } catch {
      client.disconnect(true);
    }
  }
}

Modeling the Authenticated User

Storing client.data.user as an untyped object invites typos later. Define a small interface and a typed helper so every handler reads user.id and user.role with full IntelliSense and compile-time safety.

This is the same JWT payload pattern you use for HTTP routes, keeping authorization logic consistent across both transports.

export interface SocketUser {
  id: string;
  role: 'admin' | 'member' | 'guest';
}

export interface JwtPayload {
  sub: string;
  role: SocketUser['role'];
  exp: number;
}

export function toSocketUser(payload: JwtPayload): SocketUser {
  return { id: payload.sub, role: payload.role };
}

// Demo: a decoded token becomes a typed user
const payload: JwtPayload = { sub: 'u_42', role: 'member', exp: 1893456000 };
const user = toSocketUser(payload);
console.log(`${user.id} connected as ${user.role}`);

Guarding Individual Message Events

Connection-time auth proves who the user is, but some events also need authorization checks — for example, only admins can broadcast a system message. NestJS guards work on WebSocket events too; you just read the socket from the execution context.

Inside a guard, switch the context to ws, grab the client, and inspect client.data.user that handleConnection populated.

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';

@Injectable()
export class WsAuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const client = context.switchToWs().getClient<Socket>();
    const user = client.data.user;
    if (!user) {
      throw new WsException('Unauthorized');
    }
    return true;
  }
}

Role-Based Guards with Metadata

To restrict an event to certain roles, combine a custom decorator (storing required roles as metadata) with a guard that reads it via Reflector. This mirrors the HTTP @Roles() pattern, so your team learns one mental model.

The guard rejects the event with a WsException when the connected user lacks the required role — the message handler never runs.

import { SetMetadata } from '@nestjs/common';
export const WsRoles = (...roles: string[]) => SetMetadata('ws_roles', roles);

import { CanActivate, ExecutionContext, Injectable } 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 required = this.reflector.get<string[]>('ws_roles', context.getHandler());
    if (!required?.length) return true;
    const user = context.switchToWs().getClient<Socket>().data.user;
    if (!user || !required.includes(user.role)) {
      throw new WsException('Forbidden');
    }
    return true;
  }
}

Applying Guards to Subscribe Handlers

Attach guards to message handlers with @UseGuards(), exactly like controller routes. Stack the base auth guard with the roles guard, and decorate the handler with the required roles.

Guards run in declaration order, so put the cheaper authentication check first and the role check second.

import { SubscribeMessage, WebSocketGateway, MessageBody } from '@nestjs/websockets';
import { UseGuards } from '@nestjs/common';
import { WsAuthGuard } from './ws-auth.guard';
import { WsRolesGuard } from './ws-roles.guard';
import { WsRoles } from './ws-roles.decorator';

@WebSocketGateway()
export class AdminGateway {
  @UseGuards(WsAuthGuard, WsRolesGuard)
  @WsRoles('admin')
  @SubscribeMessage('broadcast')
  handleBroadcast(@MessageBody() text: string) {
    return { event: 'broadcast', data: text };
  }
}

Why Guards Alone Miss the Handshake

A subtle gotcha: by default a WebSocket guard runs on message events, not on the initial connection. If you rely only on @UseGuards and skip handleConnection, an unauthenticated client can still open a socket and sit idle in your server.

  • Use handleConnection to reject unauthenticated sockets at the door.
  • Use event guards for fine-grained, per-action authorization.

The two layers complement each other: one controls entry, the other controls actions.

Surfacing Errors Cleanly to Clients

When a guard throws WsException, NestJS emits an exception event to that client instead of crashing the connection. Add a WsExceptionFilter to shape the payload so the frontend gets a predictable error object it can show to the user.

import { ArgumentsHost, Catch } from '@nestjs/common';
import { BaseWsExceptionFilter, WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';

@Catch(WsException)
export class WsErrorFilter extends BaseWsExceptionFilter {
  catch(exception: WsException, host: ArgumentsHost) {
    const client = host.switchToWs().getClient<Socket>();
    client.emit('error', {
      message: exception.getError(),
      timestamp: new Date().toISOString(),
    });
  }
}

Handling Token Expiry on a Live Socket

A connection authenticated an hour ago may now hold an expired token. Two common strategies:

  • Re-verify per sensitive event — store the raw token on client.data.token and call verifyAsync again inside the guard for high-value actions.
  • Periodic revalidation — a server interval checks each socket's token exp and disconnects expired ones.

This small pure function shows the expiry check at the heart of either approach.

interface TokenInfo {
  exp: number; // unix seconds
}

function isExpired(token: TokenInfo, nowSeconds: number): boolean {
  return token.exp <= nowSeconds;
}

const now = 1_700_000_000;
console.log(isExpired({ exp: 1_699_999_000 }, now)); // true  -> disconnect
console.log(isExpired({ exp: 1_700_500_000 }, now)); // false -> keep open

Wiring It Together in the Module

Guards that inject services like Reflector or JwtService must be resolvable by Nest's DI. Because the gateway and its guards live in the same module, register JwtModule and provide the gateway; the guards are instantiated by Nest when referenced in @UseGuards.

You can also register the auth guard globally for sockets with APP_GUARD if every event should be authenticated by default.

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { APP_GUARD } from '@nestjs/core';
import { ChatGateway } from './chat.gateway';
import { WsAuthGuard } from './ws-auth.guard';

@Module({
  imports: [
    JwtModule.register({ secret: process.env.JWT_SECRET }),
  ],
  providers: [
    ChatGateway,
    { provide: APP_GUARD, useClass: WsAuthGuard },
  ],
})
export class RealtimeModule {}

Quick Check: Connection vs Event Auth

You want to (1) block unauthenticated clients from ever opening a socket and (2) allow only admin users to emit a broadcast event. Which combination correctly achieves both?

Recap: Securing Realtime Connections

You now have a complete, layered approach to securing NestJS WebSocket gateways:

  • Extract the token consistently from handshake.auth, headers, or query.
  • Authenticate at the door in handleConnection, disconnecting invalid clients and attaching the user to client.data.
  • Authorize per event with guards that switch to the ws context, including role checks via Reflector and a @WsRoles decorator.
  • Report errors through a WsException filter, and handle expiry with re-verification or periodic checks.

Connection auth controls entry; event guards control actions — together they keep your realtime system secure.

자주 묻는 질문

“소켓 연결 인증 및 보호” 강의는 무료인가요?

네 — “소켓 연결 인증 및 보호” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 NestJS Enterprise Backend APIs 강의 전체를 잠금 해제할 수 있습니다. NestJS Enterprise Backend APIs 강의에는 총 4개의 강의가 포함되어 있습니다.

“소켓 연결 인증 및 보호”에서 뭘 배우나요?

안전한 실시간 액세스를 위해 핸드셰이크와 메시지 이벤트에 가드 및 토큰 검증을 적용합니다 브라우저에서 직접 실행하는 실습 코드로 NestJS Enterprise Backend APIs을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

NestJS Enterprise Backend APIs을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 NestJS Enterprise Backend APIs은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“소켓 연결 인증 및 보호” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 NestJS Enterprise Backend APIs 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 NestJS Enterprise Backend APIs 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. Socket.IO 어댑터를 사용한 WebSocket 게이트웨이
  2. 소켓 연결 인증 및 보호
  3. 단방향 푸시를 위한 서버 전송 이벤트
  4. Redis Pub/Sub 어댑터로 실시간 기능 확장하기
← NestJS Enterprise Backend APIs(으)로 돌아가기