← 뒤로
프로젝트 요약
- 한 줄 요약: WebSocket 인증·인가·예외 처리 체계를 전반적으로 개선하여 보안을 강화하고, 클라이언트가 일관되게 복구·응답을 처리할 수 있도록 했습니다.
- 진행/소속: 개인 프로젝트
- 키워드:
WebSocket, 인증, 인가, STOMP, 예외 처리, 토큰 갱신
- 역할: 백엔드
문제(AS-IS)

- 인가되지 않은 사용자가 다른 사용자 채널을 구독할 수 있는 가능성
- 인가가 취소된 사용자가 계속 메시지를 수신할 수 있는 가능성
- 토큰을 쿼리 파라미터로 전달해 서버 로그 등에 노출될 위험
- REST API와 달리 예외 응답 형식이 정해져 있지 않아 클라이언트가 예외 파악·복구 조치가 어려움
- 연결 유지 중 토큰 만료/만료 임박 시 처리 체계 부재
목표(TO-BE)
- 권한 기반 메시지 전달: 사용자 권한에 따라 메시지가 전달되도록 하여, 인가가 취소된 사용자의 메시지 수신 가능성 제거
- 인증 정보 노출 최소화: 인증 정보가 서버 로그 등에 노출되지 않도록 전달 방식 개선
- 구독 권한 검증: 구독 대상에 대한 접근 권한을 검증하여, 인가되지 않은 구독 차단
- 일관된 예외·성공 응답 템플릿: REST API와 유사하게 정해진 형식으로 예외·성공 응답 제공
- 토큰 수명 주기 관리: 만료 시 즉시 종료, 만료 임박 시 갱신 유도로 연결 유지
- 문서화: 프로토콜 설계를 문서로 정리하여 쉽게 이해할 수 있도록 제공
설계/선택(Key decisions)
- 권한 기반 전달: 유저별 전용 채널
/topic/user/{uuid}/... — 권한 회수 시 전송 차단·사용자 단위 스트림 통일
- 인증: CONNECT 시 헤더 토큰.
/app/auth/refresh 토픽으로 auth 갱신
- 구독 검증: topic uuid = principal 검증, 불일치 시 거부
- 응답 형식:
/queue/session/{sessionId}/exception (예외 큐) 구현
/queue/session/{sessionId}/reply (응답/결과 큐) 구현
- 만료 처리: 만료 → 인바운드·Heartbeat 검사 후 ERROR·종료. 만료 임박 → auth 큐로
token_expiring_soon 전송, 클라이언트 갱신
결과(Impact)

- 권한 기반 전달: 인가 취소 시 해당 유저 채널로 미전송. 타 유저 채널 구독 차단
- 토큰 노출 최소화: CONNECT 시 헤더 전달, 로그/Referer 노출 위험 감소
- 예외 파악 용이: 클라이언트가
code, message, details로 예외 유형·복구 방법을 식별 가능
- 요청–응답 매칭:
receiptId, requestDestination으로 SEND–ERROR/Reply 매칭 가능
- 연결 유지: 만료 임박 시 갱신 유도로 재연결 없이 토큰 갱신 후 연결 유지
- 문서 일원화: AsyncAPI·websocket-docs에 인증 규칙, 예외·성공 응답 형식, 갱신 API·테스트 절차 반영
아래 링크에서 실제 구현 확인 가능
message.rahoon.site/websocket-docs
구현 상세
1) 권한 기반 메시지 전달·구독 검증
배경: 기존에는 채팅방·토픽 단위로 브로드캐스트하고 있어, 한 번 구독한 사용자는 채팅방에서 나가더라도(인가가 취소되어도) 계속 메시지를 받을 수 있었다. “이 유저에게만” 전달하는 단위가 없어서, 권한 회수 시 수신을 끊을 수 없었다.
후보 검토: (A) 유저별 전용 채널, (B) 채팅방 토픽 + 구독 시 권한 검증만, (C) A와 B 혼합.
- B는 구독 후 권한 취소 시 대응이 어렵고, C는 토픽 종류가 늘어나 복잡해진다.
- Discord·Firebase를 참고해 A. 유저별 전용 채널을 선택했다.
- 채팅방도 “참여자 각자의 유저 채널로 전송”하면 되고, 기존 Redis Pub/Sub 구조로 그대로 라우팅 가능했다.
트레이드오프: 채팅방 메시지는 참여자 수만큼 유저 채널로 전송되므로 트래픽이 늘 수 있음. 수평 확장·Throughput 개선으로 상쇄 가능한 수준으로 판단.
2) 인증·갱신
배경:
- 토큰을 쿼리 파라미터로 넘기면 서버 로그·Referer에 노출된다.
- Handshake 시 HTTP 헤더로 넘기는 옵션이 있지만, 브라우저 기본 WebSocket API와 SockJS·STOMP 클라이언트 라이브러리가 헤더 지정을 잘 지원하지 않아 현실적으로 불가에 가깝다.
선택: STOMP CONNECT 시 헤더로 토큰 전달.
- Handshake는 토큰 없이 하고, STOMP 단에서만 인증.
- 인증 실패 시 ERROR 후 연결 종료.
갱신 API: 토큰 만료 전에 재연결 없이 갱신할 수단이 필요했다.
/app/auth/refresh destination으로 SEND 시, 헤더(Authorization) 또는 Body(accessToken)로 새 토큰 전달.
- Body 옵션은 헤더를 지정하기 어려운 클라이언트(일부 라이브러리, 네이티브 앱 등)를 위한 것이다.
주의: auth 큐(/queue/session/{sessionId}/auth) 구독 시 sessionId는 CONNECTED 프레임의 session 헤더 값을 써야 한다. StompSession.getSessionId()는 서버 session id와 다를 수 있음.
3) 만료·만료 임박
배경:
- JWT 토큰은 유효 기간이 있어, WebSocket 연결이 길어지면 연결 중에 만료될 수 있다.
- 만료된 토큰으로 계속 메시지를 수신할 수 있다면 보안 문제가 발생한다
- 만료 시점에 갑자기 끊기면 UX가 나쁠 것으로 예상된다
- 만료 직전에 갱신 기회를 주는 것이 좋지만, 이미 만료된 경우는 갱신이 불가능하므로 즉시 종료해야 한다.
선택: “만료”와 “만료 임박”을 구분하여 처리한다.
- 만료:
- 인바운드 메시지 수신 시 + Heartbeat 주기마다 만료 검사 → 만료 시 ERROR를 전송하여 DISCONNECT
- 만료 임박:
- 만료까지
IMMINENT_THRESHOLD 남은 세션 대상으로 Heartbeat 주기마다 /queue/session/{sessionId}/auth로 token_expiring_soon MESSAGE 전송.
websocket.imminent-threshold-seconds(기본 120초)로 환경별 조정 가능.
4) 예외·성공 응답
배경:
- STOMP 스펙에는 SEND에 대한 “성공 응답 body”가 없다.
- 실패 시 ERROR만 있고, ERROR 수신 후 서버는 연결 종료한다.
- REST API처럼 예외·성공 응답 형식을 정해두면 클라이언트가 예외 유형·복구 방법을 파악하기 쉬워진다.
선택:
- 요청에 대한 응답을 보낼 수 있는
/queue/session/{sessionId}/exception 구현
- 요청 처리 실패 시 연결을 종료하지 않고 예외만
/queue/session/{sessionId}/exception로 던지도록 개선
- 위와 같은 코드의 보일러플레이트를 줄이기 위한 어노테이션 및 글로벌 에러 핸들러 선언
5) 문서·주요 클래스
AsyncAPI info.description과 websocket-docs에 인증 규칙, 구독 권한, 갱신 API, 예외·성공 응답 형식에 대한 설명 추가