문제 상황
로컬 환경에서 배포된 서버 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
'Backend > 인증, 인가' 카테고리의 다른 글
OAuth2.0 인트로 (0) | 2024.10.15 |
---|---|
JWT(JSON Web Token)와 세션 방식을 쉽게 비교해보자 (0) | 2023.05.30 |