Nest.js를 배워보자/11. NestJS WebSocket 실전 — 실시간 시스템 만들기

인증된 WebSocket 연결 (심화)

_Blue_Sky_ 2025. 12. 3. 11:13
728x90

HTTP 기반의 REST API와 달리, WebSocket은 초기 핸드셰이크(Handshake) 시에만 인증 정보를 확인하며, 이후에는 이 연결 상태를 지속적으로 유지해야 합니다. NestJS에서는 GuardPipe를 사용하여 HTTP와 유사하게 인증을 처리할 수 있습니다.

1. 클라이언트 측에서 토큰 전달

클라이언트는 연결을 시도할 때 JWT 토큰을 Socket.io의 auth 옵션을 통해 서버로 전달합니다.

// 클라이언트 (예: Vue, React, Plain JS)
const token = 'YOUR_VALID_JWT_TOKEN';

const socket = io('http://localhost:8080/chat', {
  auth: {
    token: token, // 서버의 Gateway로 전달됨
  }
});

 

728x90

2. Gateway에서 연결 시 인증 처리

NestJS Gateway의 handleConnection 메서드는 연결이 수립되기 직전에 호출되므로, 이 곳에서 토큰을 검증하고 사용자 정보를 소켓 객체에 저장하는 것이 베스트 프랙티스입니다.

// src/auth/ws-auth.guard.ts (인증 로직을 분리한 Guard 예시)

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';
import * as jwt from 'jsonwebtoken';
// 실제 프로젝트에서는 JwtService를 주입받아 사용해야 합니다.

@Injectable()
export class WsAuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean | Promise<boolean> {
    const client: Socket = context.switchToWs().getClient<Socket>();
    const token = client.handshake.auth.token;

    if (!token) {
      throw new WsException('인증 토큰이 누락되었습니다.');
    }

    try {
      // 1. 토큰 검증 (실제 SECRET_KEY 사용)
      const payload = jwt.verify(token, 'YOUR_SECRET_KEY'); 

      // 2. 인증 성공 시 사용자 정보를 소켓 객체에 저장
      // 이렇게 저장된 정보는 @SubscribeMessage 핸들러에서 사용 가능합니다.
      client['user'] = payload; 
      return true;

    } catch (e) {
      // 3. 토큰이 유효하지 않으면 WsException 발생 -> 연결 거부
      throw new WsException('유효하지 않거나 만료된 토큰입니다.');
    }
  }
}

 

728x90

3. 메시지 핸들러에서 사용자 정보 사용

handleConnection에서 인증을 완료하고 사용자 정보를 소켓(client['user'])에 저장했으므로, 이제 @SubscribeMessage 메서드에서 이 정보를 안전하게 사용할 수 있습니다.

// src/chat/chat.gateway.ts (Gateway 내부)
// ...
import { UseGuards } from '@nestjs/common';
import { WsAuthGuard } from './ws-auth.guard'; 

@WebSocketGateway(...)
export class AuthenticatedChatGateway implements OnGatewayConnection, ... {
    // ... handleConnection은 WsAuthGuard의 canActivate가 대신 처리합니다.

    // 1. Gateway 전체에 Guard 적용 (모든 메시지 이벤트에 인증 필요)
    @UseGuards(WsAuthGuard) 
    handleConnection(client: Socket) {
        // Guard가 true를 반환했으므로, 이미 인증된 상태입니다.
        const user = client['user'];
        this.logger.log(`인증된 사용자(${user.userId}) 연결: ${client.id}`);
    }

    // 2. 특정 메시지 이벤트에 Guard 적용 (선택적)
    @UseGuards(WsAuthGuard)
    @SubscribeMessage('sendMessage')
    handleMessage(
        @MessageBody() data: { message: string },
        @ConnectedSocket() client: Socket,
    ) {
        // 소켓 객체에서 저장된 사용자 정보 추출
        const user = client['user']; 

        // 사용자 인증 정보를 기반으로 메시지 처리
        this.logger.log(`[${user.username}] 메시지 수신: ${data.message}`);
        
        // ... 메시지 브로드캐스팅 로직
    }
}

🔑 요약 베스트 프랙티스

  1. 토큰 전달: 클라이언트는 반드시 socket.io의 auth 객체를 통해 JWT를 전달합니다.
  2. 인증 분리: 인증 로직은 Gateway 메서드 내부에 직접 작성하기보다, WsAuthGuard 클래스로 분리하여 재사용성과 유지보수성을 높입니다.
  3. 예외 처리: 인증 실패 시 일반적인 client.disconnect() 대신 WsException을 사용하여 클라이언트에게 명확한 오류 코드를 전달해야 합니다.
  4. 정보 저장: 인증에 성공하면, 사용자 페이로드(payload)를 소켓 객체(client['user'])에 저장하여 다른 @SubscribeMessage 핸들러에서 쉽게 접근할 수 있도록 합니다.

 

728x90