1. 문제 상황
1.1 발생한 문제
SSE(Server-Sent Events)를 통한 로그 스트리밍 연결 시 다음과 같은 오류가 발생했습니다.
HTTP/1.1 401 Unauthorized문제 증상
- 일반 REST API 요청은 정상 작동 (JWT 인증 성공)
- SSE 연결만 401 Unauthorized 발생
- EventSource를 통한 실시간 로그 스트리밍 불가
1.2 환경 정보
기술 스택
- Spring Boot 3.x
- Spring Security
- JWT 인증
- SSE (Server-Sent Events)
인증 구조
클라이언트 → Authorization: Bearer <JWT> → JwtAuthenticationFilter → SecurityContext2. 원인 분석
2.1 EventSource API의 제약사항
EventSource란?
EventSource는 브라우저에서 서버로부터 단방향 실시간 스트리밍을 받기 위한 API입니다.
주요 특징
- 서버 → 클라이언트 방향의 단방향 통신
- HTTP 프로토콜 기반
- 자동 재연결 기능
제약
// ❌ EventSource는 커스텀 헤더를 설정할 수 없음
const eventSource = new EventSource('/api/logs/stream', {
headers: {
'Authorization': 'Bearer ' + token // 불가능!
}
});2.2 JWT 인증의 일반적인 흐름
정상적인 REST API 요청
1. 클라이언트가 Authorization: Bearer <JWT> 헤더 포함하여 요청
↓
2. JwtAuthenticationFilter가 헤더에서 토큰 추출
↓
3. 토큰 유효성 검증
↓
4. SecurityContext에 인증 객체 저장
↓
5. 요청 처리SSE 요청의 문제
1. EventSource로 연결 시도 (Authorization 헤더 없음)
↓
2. JwtAuthenticationFilter가 토큰을 찾지 못함
↓
3. 인증 실패
↓
4. HTTP 401 Unauthorized 반환2.3 기존 JwtAuthenticationFilter 코드
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null; // ❌ SSE 요청 시 항상 null 반환
}문제점
- Authorization 헤더만 확인
- EventSource는 헤더를 보낼 수 없으므로 항상 null 반환
- 결과적으로 인증 실패
3. 해결 방법
3.1 핵심 아이디어
쿼리 파라미터를 통한 토큰 전달
EventSource의 헤더 설정 제약을 우회하기 위해 JWT 토큰을 URL의 쿼리 파라미터로 전달합니다.
// ✅ 쿼리 파라미터로 토큰 전달 가능
const token = localStorage.getItem('accessToken');
const eventSource = new EventSource(`/api/logs/stream?token=${token}`);표준 방식
- WebSocket, SSE 등 커스텀 헤더를 지원하지 않는 통신에서 표준적으로 사용되는 방법
- OAuth 2.0 등에서도 유사한 방식 사용
3.2 JwtAuthenticationFilter 수정
수정된 코드
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_PREFIX = "Bearer ";
private static final String TOKEN_QUERY_PARAM = "token"; // 추가
private final JwtTokenProvider jwtTokenProvider;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 토큰 추출 (헤더 또는 쿼리 파라미터)
String token = resolveToken(request);
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
/**
* HTTP 헤더 또는 쿼리 파라미터에서 JWT 토큰 추출
*/
private String resolveToken(HttpServletRequest request) {
// 1️⃣ Authorization 헤더에서 토큰 추출 (일반 API 요청)
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(BEARER_PREFIX.length());
}
// 2️⃣ 쿼리 파라미터에서 토큰 추출 (SSE 요청)
String requestUri = request.getRequestURI();
if (requestUri.startsWith("/api/logs/stream")) {
String tokenParam = request.getParameter(TOKEN_QUERY_PARAM);
if (StringUtils.hasText(tokenParam)) {
return tokenParam;
}
}
return null;
}
}3.3 작동 원리
처리 순서
1. Authorization 헤더 확인
↓
있으면 → Bearer 토큰 추출
없으면 ↓
2. token 쿼리 파라미터 확인
↓
있으면 → 토큰 값 추출
없으면 ↓
3. null 반환 (인증 실패)예시
일반 API 요청:
GET /api/logs
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
→ 헤더에서 토큰 추출 ✅
SSE 요청:
GET /api/logs/stream?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
→ 쿼리 파라미터에서 토큰 추출 ✅3.4 프론트엔드 코드 수정
React 예시
// 기존 코드 (실패)
const eventSource = new EventSource('/api/logs/stream');
// 수정된 코드 (성공)
const token = localStorage.getItem('accessToken');
const eventSource = new EventSource(`/api/logs/stream?token=${token}`);
eventSource.onmessage = (event) => {
const log = JSON.parse(event.data);
console.log('Received log:', log);
};
eventSource.onerror = (error) => {
console.error('SSE connection error:', error);
eventSource.close();
};6. 결론
6.1 핵심 요약
| 항목 | 내용 |
|---|---|
| 문제 | EventSource는 Authorization 헤더를 보낼 수 없어 JWT 인증 실패 |
| 원인 | EventSource API의 제약으로 커스텀 헤더 설정 불가 |
| 해결 | 쿼리 파라미터로 JWT 전달 + 서버 필터 수정 |
| 결과 | SSE 스트리밍에서도 정상 인증 처리, 401 오류 해결 |