Skip to content

241120 cookie websocket

정동교 edited this page Nov 20, 2024 · 3 revisions

Cross-Origin WebSocket에서 쿠키 전송 문제: 삽질 기록과 해결

J221 정동교, J262 최정민

문제 상황

로컬 환경에서 배포된 서버 Backend의 WebSocket으로 접근하는 상황이었다. 인증 정보를 쿠키로 전달하려 했지만, WebSocket 연결이 완료되지 못했다.

bet.gateway.ts

 handleConnection(client: Socket) {
    try {
      const cookies = this.jwtUtils.parseCookies(
        client.handshake.headers.cookie,
      );
      const accessToken = cookies["access_token"];
      if (!accessToken) {
        client.emit("error", {
          event: "handleConnection",
          message: "엑세스 토큰이 존재하지 않습니다.",
        });
        client.disconnect(true);
        return;
      }

     ...
  }

bet.gateway.ts에서는 위의 로직으로 accessToken을 검사한다. 그리고 accessToken이 없다면 client.disconnect(true); 를 통해 소켓을 끊어버린다.

브라우저의 개발자 도구에서 응답의 헤더를 확인해보니 Cookie가 비어있었다. 그런 이유로 서버에서 client.handshake.headers.cookie를 조회할 때에도 undefined가 출력되었다.


삽질의 시작

문제 원인을 찾기 위해 Nginx 설정, CORS 설정, Socket.IO 설정 등을 모두 점검했지만 문제는 해결되지 않았다.

시도한 방법들

Nginx CORS 설정 추가

add_header 'Access-Control-Allow-Origin' 'http://localhost:3000';
add_header 'Access-Control-Allow-Credentials' 'true';

Socket.IO Gateway 설정

@WebSocketGateway({
  namespace: "api/betting",
  cors: {
    origin: ["http://localhost:3000", "http://175.45.205.245"],
    credentials: true,
  },
})

CORS, Nginx, Socket.IO 모두 올바르게 설정되었지만 쿠키는 여전히 포함되지 않았다.


문제 원인 파악

쿠키의 SameSite 속성

BE (user.controller.ts)

    res.cookie("access_token", result.accessToken, {
      httpOnly: true,
      maxAge: 1000 * 60 * 60,
      secure: false, // HTTPS를 통해서만 전송되도록 설정
      // sameSite: "strict", // 기본 값 LAX
    });

BE에서 쿠키는 위와 같이 생성된다.

sameSite 설정을 따로 하지 않으면 기본값은 SameSite=Lax 로 설정된다.

그런데 Lax 설정은 예외 상황을 제외하고는 Cross-Origin 요청에서 쿠키를 포함하지 않는다. (예외 상황: GET 요청 + 탑 레벨 네비게이션 (주소창 입력, 링크 클릭 등))

그런 이유로 POST, PUT, WebSocket 연결 등의 요청에서는 Lax 쿠키가 포함되지 않게 된다.

결론은 SameSite 설정 문제였다!


해결 방법

(1) 쿠키의 SameSite 설정 변경

SameSite=None + Secure=true를 사용하여 쿠키가 Cross-Origin 요청에도 포함되도록 설정하는 방법이 있다.

BE (user.controller.ts)

res.cookie("access_token", token, {
  httpOnly: true,
  secure: true, // HTTPS 필수
  sameSite: "None", // Cross-Origin 허용
});

하지만 secure: true를 설정하면, HTTPS 환경 필수인데 현재의 서버는 HTTPS로 구성되어 있지 않았다.


(2) JWT 토큰 기반 인증으로 대체

쿠키 대신 토큰을 사용하여 인증을 진행한다.

FE

import { io } from "socket.io-client";

const socket = io(SOCKET_URL + options.url, {
  auth: {
    token: `${access_token}`,
  },
});

BE

handleConnection(client: Socket) {
  const token = socket.handshake.auth.token;
  const payload = this.jwtUtils.verifyToken(token);
  client.data.userId = payload.id;
}

토큰을 이용한 방식으로 에러를 해결했다.


추가로 참고하면 좋을 사항

토큰을 이용한 방식으로 처리할 때 가장 처음 custom header를 사용했다.

FE

const socket = io(SOCKET_URL + options.url, {
  transports: ["websocket"],
  extraHeaders: {
    Authorization: `Bearer ${accessToken}`, // 커스텀 헤더 만들기
  },
  withCredentials: true,

BE

  handleConnection(client: Socket) {
    try {
      const authorizationHeader = client.handshake.headers.authorization;
    ...

하지만 http→websocket upgrade를 위한 handshake http 요청에는 custom header를 달 수 없다!

이와 관련된 참고자료

https://socket.io/docs/v4/server-socket-instance/#sockethandshake

https://github.com/whatwg/websockets/issues/16

https://stackoverflow.com/questions/23406163/socket-io-client-how-to-set-request-header-when-making-connection

🏠 𝐇𝐨𝐦𝐞

🙌 𝐈𝐧𝐭𝐫𝐨𝐝𝐮𝐜𝐭𝐢𝐨𝐧

📄 𝐏𝐫𝐨𝐣𝐞𝐜𝐭 𝐃𝐨𝐜𝐮𝐦𝐞𝐧𝐭𝐚𝐭𝐢𝐨𝐧

🤝 𝐓𝐞𝐚𝐦 𝐑𝐮𝐥𝐞𝐬

🎁 𝐓𝐞𝐜𝐡𝐧𝐢𝐜𝐚𝐥 𝐒𝐡𝐚𝐫𝐢𝐧𝐠

프로젝트 초기 세팅

메인 페이지

로그인/회원가입/게스트 로그인

베팅페이지

BE

FE

🔫 𝐓𝐫𝐨𝐮𝐛𝐥𝐞𝐬𝐡𝐨𝐨𝐭𝐢𝐧𝐠

📎 𝐑𝐞𝐟𝐞𝐫𝐞𝐧𝐜𝐞 𝐋𝐢𝐧𝐤𝐬

Clone this wiki locally