
Spring Security JSESSIONID가 보이면 stateless JWT 설정이 실패한 것처럼 느껴집니다. 하지만 브라우저에 JSESSIONID 쿠키가 생겼다는 사실만으로 Spring Security가 인증 세션을 사용한다고 단정하면 안 됩니다.
먼저 나눠야 할 기준은 누가 세션을 만들었고, 그 세션이 인증 상태 저장에 실제로 쓰이는가입니다. Spring Security 설정, CSRF, OAuth2 login, servlet container를 따로 봐야 원인이 보입니다.

Spring Security JSESSIONID를 볼 때 첫 질문
첫 질문은 단순합니다. 이 JSESSIONID가 인증을 유지하기 위해 쓰이고 있는가, 아니면 다른 웹 기능 때문에 만들어졌는가입니다. stateless JWT API에서는 요청마다 Authorization header의 bearer token을 검증하는 흐름이 중심입니다.
반면 브라우저 기반 로그인, CSRF token 저장, OAuth2 authorization request 저장, 서버 템플릿 화면 등은 세션을 만들 수 있습니다. API 인증은 stateless여도 같은 애플리케이션 안의 다른 흐름이 세션을 사용할 수 있습니다.
SessionCreationPolicy.STATELESS의 의미
SessionCreationPolicy.STATELESS는 Spring Security가 SecurityContext를 저장하거나 얻기 위해 HttpSession을 만들거나 사용하지 않겠다는 뜻입니다. 즉, 인증 정보를 세션에 저장하지 않겠다는 정책입니다.
@Bean
SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.build();
}하지만 이 설정이 애플리케이션 전체에서 어떤 코드도 HttpSession을 만들지 못하게 막는다는 뜻은 아닙니다. 다른 filter, controller, view, 라이브러리 코드가 session에 접근하면 container가 쿠키를 만들 수 있습니다.
CSRF 설정을 확인한다
브라우저 쿠키 기반 인증에서는 CSRF 보호가 중요합니다. 하지만 Authorization header 기반 stateless API에서는 상황이 다를 수 있습니다. 설정이 섞이면 CSRF token 저장 과정에서 세션이 생기는지 확인해야 합니다.
- 순수 bearer token API라면 CSRF disable을 검토한다
- 쿠키에 access token을 담는 구조라면 CSRF 위험을 다시 평가한다
- 관리자 화면과 API를 같은 SecurityFilterChain으로 묶지 않는다
- API와 웹 로그인 흐름을 분리하면 원인 추적이 쉬워진다
OAuth2 login은 세션을 만들 수 있다
OAuth2 login은 브라우저 redirect를 거치는 웹 로그인 흐름입니다. authorization request, state 값, 로그인 성공 후 처리 과정에서 세션 저장소를 사용할 수 있습니다.
따라서 소셜 로그인과 JWT API 인증을 같은 체인에서 섞으면 ‘stateless로 했는데 왜 세션이 생기지?’라는 상황이 생길 수 있습니다. 이때는 소셜 로그인 완료 후 자체 JWT를 발급하고, 이후 API 인증은 별도 stateless 체인으로 분리하는 구조를 검토합니다.
원인 찾는 체크리스트
- JSESSIONID가 어떤 요청에서 처음 내려오는지 Network 탭에서 확인한다
- 응답 header의 Set-Cookie가 API 요청인지 로그인 redirect인지 구분한다
- SecurityFilterChain이 API와 웹 로그인에 분리되어 있는지 본다
- Controller나 filter에서 request.getSession()을 호출하는 코드가 있는지 검색한다
- CSRF token repository, remember-me, OAuth2 login 설정을 확인한다
실무 구성 예시
한 애플리케이션 안에서도 API와 웹 로그인 흐름을 분리하면 혼란이 줄어듭니다. 예를 들어 /api/**는 JWT stateless 체인으로, /oauth2/**와 /login/**은 OAuth2 login 체인으로 나눠볼 수 있습니다.
@Bean
@Order(1)
SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(a -> a.anyRequest().authenticated())
.build();
}핵심 기준
JSESSIONID가 보인다고 바로 stateless JWT 설정이 실패한 것은 아닙니다. 먼저 그 세션이 인증 상태를 저장하는 데 쓰이는지, 아니면 다른 웹 기능 때문에 만들어졌는지를 나눠 봐야 합니다.
Spring Security는 STATELESS 설정을 통해 SecurityContext 저장에 HttpSession을 쓰지 않도록 할 수 있습니다. 하지만 애플리케이션의 다른 코드나 웹 로그인 흐름이 세션을 만들 수는 있습니다.

가장 먼저 Network 탭을 본다
브라우저에서 JSESSIONID가 보이면 Set-Cookie가 내려온 정확한 요청을 찾아야 합니다. 로그인 redirect에서 생겼는지, API 호출에서 생겼는지에 따라 원인이 완전히 달라집니다.
- 개발자 도구 Network 탭을 연다
- Preserve log를 켠다
- 로그인 또는 API 요청을 다시 실행한다
- Set-Cookie: JSESSIONID가 있는 응답을 찾는다
- 그 요청의 URL, status, redirect 여부를 기록한다
SecurityFilterChain을 분리한다
API와 웹 로그인 흐름을 한 체인에 섞으면 원인 추적이 어렵습니다. /api/**는 JWT stateless 체인으로, /oauth2/**나 /login/**은 웹 로그인 체인으로 분리하면 판단이 쉬워집니다.
@Bean
@Order(1)
SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.build();
}CSRF 때문에 생기는지 확인한다
CSRF 보호는 쿠키 기반 브라우저 인증에서 중요합니다. 하지만 Authorization header로 bearer token을 보내는 API라면 CSRF 설정을 다르게 볼 수 있습니다.
- API가 cookie 인증인지 bearer token 인증인지 먼저 나눈다
- CSRF token repository가 session을 쓰는지 확인한다
- 관리자 웹 화면과 API를 같은 체인에 두지 않는다
- 쿠키에 JWT를 담는 구조라면 CSRF를 다시 평가한다
코드에서 session 접근을 검색한다
Spring Security 설정이 맞아도 직접 request.getSession()을 호출하면 세션이 만들어질 수 있습니다. interceptor, filter, controller, view helper까지 검색해야 합니다.
검색 키워드:
getSession(
HttpSession
@SessionAttributes
session.
rememberMe
테스트로 확인하는 방법
수동 확인만으로는 배포 후 다시 섞일 수 있습니다. API 요청에 대해 Set-Cookie가 내려오지 않는지 통합 테스트를 추가하면 회귀를 줄일 수 있습니다.
mockMvc.perform(get("/api/me")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token))
.andExpect(status().isOk())
.andExpect(header().doesNotExist(HttpHeaders.SET_COOKIE));최종 확인 체크리스트
- API 요청에서 Set-Cookie가 나오면 API 체인부터 확인한다
- OAuth2 redirect에서 나오면 로그인 흐름의 저장소를 확인한다
- CSRF token이 필요 없는 API라면 설정을 분리한다
- 세션이 보이는 것과 인증 세션을 쓰는 것은 다르다
정리
JSESSIONID가 보인다고 곧바로 JWT stateless 설정이 실패했다고 판단하면 원인을 놓치기 쉽습니다. 중요한 것은 해당 세션이 인증 상태 저장에 쓰이는지, 아니면 다른 웹 흐름 때문에 생긴 것인지 구분하는 일입니다.
관련해서 내부 글은 Spring Boot JWT 인증 구현, Spring Security JWT와 OAuth2 Resource Server는 어떻게 다를까, OAuth2 소셜 로그인 후 JWT 발급 구조와 함께 보면 좋습니다.
외부 기준은 Spring Security API – SessionCreationPolicy, Spring Security Reference – CSRF, Spring Security Reference – OAuth2 Login를 기준으로 확인했습니다.