프론트엔드(Vercel + Next.js)와 백엔드(EC2 + Spring Boot)를 분리 배포하면서, WebSocket 기반 채팅 기능에서 인증이 되지 않는 문제가 발생했습니다. 다른 API 호출은 정상 동작했지만, 채팅 연결 시에만 Principal이 null이 되는 예외가 발생했습니다.
문제 정의
- REST API 호출은 모두 정상 인증됨 (쿠키 기반 JWT 전달).
- STOMP(WebSocket) 연결 시 No Principal found 에러 발생.
- 서버 로그 분석 결과 Authenticated=false 또는 Principal=null 상태로 들어오는 상황
- 네트워크 탭에서 WS 요청 로그를 확인해보니, CONNECT 프레임에는 쿠키가 전달되지 않음.
즉, API 요청은 쿠키 인증이 정상인데 WebSocket 연결만 실패하는 상황이었습니다.
원인 분석
1. 쿠키의 도메인 스코프 문제
현재 프로젝트에서 토큰 인증구조는 다음과 같습니다.
- 프론트 요청 → Vercel 도메인(/api/**) → Next.js API Route → 서버
- 브라우저에서 직접 서버 도메인을 노출하지 않고, 토큰 설정(cookies) 같은 걸 Next.js 서버 쪽에서 처리할 수 있으며, Vercel → Spring 호출이 서버사이드라 브라우저 CORS 우회 가능한 구조입니다.
API는 정상 인증된 이유
- 프론트는 Vercel 도메인(/api/**)에서 쿠키를 심고, 서버사이드에서 Spring으로 전달
- 즉, Next.js 서버가 프록시 역할을 하며 쿠키를 붙여주기 때문에 REST는 문제 없음
채팅(WebSocket) 실패 이유
- WebSocket 연결은 브라우저 → 백엔드 도메인 직접 연결.
- 브라우저가 nip.io로 직접 요청하므로, vercel.app에 심은 쿠키가 전달되지 않음 -> 브라우저 쿠키는 도메인 스코프이기 때문.
해결 방안
1. 도메인 통합 (Proxy / Reverse Proxy)
- 프론트와 서버를 하나의 도메인 체계로 묶으면 쿠키 공유가 가능
- 또는 프론트 서버(Nginx, Vercel Edge Function 등)에서 /api/**와 /ws-stomp를 백엔드로 프록시 처리
2. 헤더로 토큰 전달
- STOMP CONNECT 시 Authorization 헤더에 JWT를 직접 전달
- 서버에서는 이 토큰을 파싱해 Principal 주입
- Spring WebSocket은 HTTP와 달리 SecurityContext에 자동으로 인증을 주입하지 않아서 STOMP CONNECT 시점에 별도로 인증 객체를 만들어 accessor.setUser(authentication)을 호출해야 함.
분리 배포 환경에서 도메인을 통일하기 어려웠고, 헤더 방식은 요청에 토큰이 눈에 보이는 형태라 네트워크 탭에서 디버깅이 쉽기에 헤더로 토큰을 전달하는 방식을 채택했습니다. 이 방식이 브라우저 정책 변화나 인프라 구조 변경에도 안정적이고, 모바일·서드파티 클라이언트까지 동일한 인증 모델을 재사용할 수 있어 운영이 단순해 실무에서도 잘 쓰인다고 합니다.
하지만, 토큰을 JS가 들고 있어야 하니 XSS에 취약해질 수 있어 액세스 토큰을 짧게하고 Refresh는 httpOnly 쿠키로만 갱신해 안전성을 보강하기로 했습니다. 따라서 STOMP의 connectHeaders.Authorization로 JWT를 명시 전달하고, 서버에서 CONNECT 인증 후 Principal 주입하는 패턴으로 수정해 인증 오류를 해결했습니다.
참고) Spring StompHandler 수정
Spring WebSocket은 HTTP와 달리 SecurityContext에 자동으로 인증을 주입하지 않아서 STOMP CONNECT 시점에 별도로 인증 객체를 만들어 accessor.setUser(authentication)을 호출해야 합니다. 쿠키가 전달되지 않으면 토큰을 파싱할 수 없고, 결과적으로 Principal=null이 되어 에러가 발생합니다.