Skip to content

๐Ÿช’ nest Websocket์— ์„ธ์…˜์ด ์•ˆ๋œ๋‹ค๊ณ ?

baegyeong edited this page Nov 29, 2024 · 1 revision
๋ถ„์•ผ ์ž‘์„ฑ์ž ์ž‘์„ฑ์ผ
BE ๊น€๋ฏผ์ˆ˜ 24๋…„ 11์›” 13์ผ

์–ด๋–ค ๋ฌธ์ œ์ธ๊ฐ€์š”?

nestjs์—์„œ websocket์—์„œ ์ฟ ํ‚ค๋ฅผ ์ „์†กํ–ˆ์„ ๋•Œ ์„ธ์…˜ ๊ฐ’์ด controller์— ์ „๋‹ฌ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

ํ•ด๋‹น ๋ฌธ์ œ๊ฐ€ ์™œ ๋ฐœ์ƒํ–ˆ๋‚˜์š”?

nestjs์—์„œ websocket server๋Š” http server์™€ ๋ณ„๋„๋กœ ๋ถ„๋ฆฌ๊ฐ€ ๋˜๊ธฐ ๋•Œ๋ฌธ์— http์— ์„ค์ •ํ•œ ๋ฏธ๋“ค์›จ์–ด๋“ค์ด ๊ณต์œ ํ•˜์ง€ ์•Š๋Š”๋‹ค. (์‹ฌ์ง€์–ด ๋ฏธ๋“ค์›จ์–ด์— ์ „๋‹ฌ๋˜๋Š” ๋งค๊ฐœ๋ณ€์ˆ˜๋„ ๋‹ค๋ฅด๋‹ค.)

๋ฌธ์ œ๋ฅผ ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ–ˆ๋‚˜์š”?

ํ•ด๋‹น ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์ „์šฉ ๊ฐ€๋“œ๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค.

๋จผ์ € HTTP์™€ Websocket ์„œ๋ฒ„์™€ ์„ธ์…˜์„ ๊ณต์œ ํ•˜๊ธฐ ์œ„ํ•ด MemoryStore๋ฅผ Provider๋กœ ์ƒ์„ฑํ–ˆ๋‹ค.

export const MEMORY_STORE = Symbol('memoryStore');

@Module({
  providers: [
    {
      provide: MEMORY_STORE,
      useFactory: () => {
        return new MemoryStore();
      },
    },
  ],
  exports: [MEMORY_STORE],
})
export class SessionModule {}

์ดํ›„ express-session์—์„œ ํ•ด๋‹น ์Šคํ† ๋ฆฌ์ง€๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ํ–ˆ๋‹ค.

// main.ts
const store = app.get(MEMORY_STORE);
app.use(session({ ...sessionConfig, store }));

์›น ์†Œ์ผ“์—์„œ ์ฟ ํ‚ค ๊ฐ’์„ ์ „๋‹ฌ๋ฐ›์œผ๋ฉด ์ฟ ํ‚ค๋ฅผ ํ•ด์„ํ•˜์—ฌ ์„ธ์…˜ ํ‚ค ๊ฐ’์„ ๊ฐ€์ ธ์™€์•ผ ๋œ๋‹ค. ์ด๋•Œ ์„ค์ •๋œ passport์—์„œ ๋‚˜ํƒ€๋‚˜๋Š” ์ฟ ํ‚ค ๊ฐ’์˜ ํ˜•์‹์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  • ์ฟ ํ‚ค ํ˜•์‹ - [์ฟ ํ‚ค์ด๋ฆ„] = s:[์ฟ ํ‚ค๊ฐ’].[๋ฌด๊ฒฐ์„ฑ ํ™•์ธ์ฝ”๋“œ]

์ด๋•Œ ๋ฌด๊ฒฐ์„ฑ ํ™•์ธ์ฝ”๋“œ๋Š” ์„ธ์…˜ ํ‚ค์˜ ๊ฐ’์„ secret์œผ๋กœ ํ•ด์‹œํ™” ํ•œ ์ฝ”๋“œ๋กœ, ๋“ค์–ด์˜จ ์ฟ ํ‚ค๊ฐ€ ์„œ๋ฒ„์—์„œ ์ƒ์„ฑํ•œ ์ฟ ํ‚ค์ธ์ง€ ํ™•์ธํ•˜๋Š” ์šฉ๋„๋กœ ์‚ฌ์šฉ๋œ๋‹ค. ๋”ฐ๋ผ์„œ ์•„๋ž˜์˜ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด์„œ ์„ธ์…˜ ํ‚ค๋ฅผ ๊ฐ€์ ธ์˜ค๋„๋ก ํ–ˆ๋‹ค.

const DEFAULT_SESSION_ID = 'connect.sid';

export const websocketCookieParse = (socket: Socket) => {
  if (!socket.request.headers.cookie) {
    throw new WsException('not found cookie');
  }
  const cookies = cookie.parse(socket.request.headers.cookie);
  const sid = cookies[sessionConfig.name || DEFAULT_SESSION_ID];
  return getSessionIdFromCookie(sid);
};

const getSessionIdFromCookie = (cookieValue: string) => {
  if (cookieValue?.startsWith('s:')) {
    const [id, signature] = cookieValue.slice(2).split('.');
    const expectedSignature = crypto
      .createHmac('sha256', sessionConfig.secret)
      .update(id)
      .digest('base64')
      .replace(/=+$/, '');

    if (expectedSignature === signature) {
      return id;
    }
    throw new WsException('Invalid cookie signature');
  }
  throw new WsException('Invalid cookie format');
};

์ฟ ํ‚ค๋ฅผ ํ†ตํ•ด์„œ ๊ฐ€์ ธ์˜จ ์„ธ์…˜ ํ‚ค ๊ฐ’์„ ํ†ตํ•œ ์ธ์ฆ์€ ์ „์šฉ ๊ฐ€๋“œ์—์„œ ์ง„ํ–‰ํ•˜๋„๋ก ํ–ˆ๋‹ค.

export interface SessionSocket extends Socket {
  session?: User;
}

export interface PassportSession extends SessionData {
  passport: { user: User };
}

@Injectable()
export class WebSocketSessionGuard implements CanActivate {
  constructor(
    @Inject(MEMORY_STORE) private readonly sessionStore: MemoryStore,
  ) {}
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const socket: SessionSocket = context.switchToHttp().getRequest();
    const cookieValue = websocketCookieParse(socket);
    const session = await this.getSession(cookieValue);
    socket.session = session.passport.user;
    return true;
  }

  private getSession(cookieValue: string) {
    return new Promise<PassportSession>((resolve, reject) => {
      this.sessionStore.get(cookieValue, (err: Error, session) => {
        if (err || !session) {
          reject(new WsException('forbidden chat'));
        }
        resolve(session as PassportSession);
      });
    });
  }
}

๐Ÿœ ํŒ€ ๊ฐœ๋ฏธ

๐Ÿ›๏ธ ํŒ€ ๋ฌธํ™”

๐Ÿ“š ํšŒ์˜๋ก

๐Ÿ”ด ์ธํ„ฐ๋ฏธ์…˜
๐ŸŸ  1์ฃผ์ฐจ
๐ŸŸก 2์ฃผ์ฐจ
๐ŸŸข 3์ฃผ์ฐจ
๐Ÿ”ต 4์ฃผ์ฐจ
๐ŸŸฃ 5์ฃผ์ฐจ
๐ŸŸค 6์ฃผ์ฐจ

๐Ÿ’ญ ํšŒ๊ณ 

๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘ ๋ฉ˜ํ† ๋ง

๐Ÿ—ฃ๏ธ ๋ฐœํ‘œ

๊ฐœ๋ฐœ ์œ„ํ‚ค

Clone this wiki locally