[🎵MIML] 리다이렉트 지옥 탈출한 썰..😵💫 (with. SessionCreationPolicy)
문제 상황
팀 프로젝트를 진행하면서 소셜 로그인 부분을 맡았다. (OAuth 제공자는 Spotify였다.)
원하는 것은 두 가지였다.
1. 보호된 uri에 접근했을 때 스포티파이 로그인으로 리다이렉트
2. 스포티파이 로그인 성공 후 다시 보호된 uri으로 리다이렉트 (당연히 authentication을 들고..🥲)
하지만 당연한게 당연하지 않았다(?)
스포티파이 로그인에 성공했음에도, 리다이렉트 되었을 때 authentication에 null이 담겨있었다. 😭
따라서 인증되지 않은 사용자로 판단하여 다시 로그인 페이지에 리다이렉트 하고,
로그인 후에는 original uri에 리다이렉트, 또 다시 로그인 페이지로 리다이렉트...
말그대로 리다이렉트 지옥에 빠져버렸다..🔥
함께 프로젝트를 진행하고 있는 팀원 중 한 분이 SecurityContextPersistenceFilter를 살펴보라고 말씀해 주셨고, 원인과 해결 방법을 찾을 수 있었다. (정말 감사합니다ㅜㅜ)
SecurityContextPersistenceFilter
SecurityContextPersistenceFilter는 SecurityFilterChain의 필터 중 하나로, 이름 그대로 SecurityContext를 영속화하는 책임을 가진 필터이다. 쉽게 말해서, 사용자의 인증(authentication) 및 권한(authorization) 정보를 관리하고 보존하는 필터이다.
기본적으로 이 필터는 세션을 사용하여 SecurityContext를 관리함으로써 요청 간에 상태 정보를 유지하는데, 동작 과정은 아래와 같다.
1. SecurityContextRepository(잘 보기..🔥)를 사용하여 이전 요청에서 저장된 SecurityContext를 가져오려고 시도한다. (복원)
2. SecurityContext(=`contextBeforeChainExecution`)가 로드되면 해당 요청의 사용자 인증 및 권한 검한 검사를 수행한다. (SecurityContextHolder를 통해 현재 스레드 내의 SecurityContext에 저장된 사용자 정보를 기반으로 수행된다.)
3. 사용자 인증 및 권한이 확인되면, 요청을 계속 진행한다. (해당 요청을 처리하는 다른 필터 및 핸들러를 호출한다.)
4. 요청 처리가 완료되면, SecurityContextRepository를 사용하여 현재 스레드의 SecurityContext(=`contextAfterChainExecution`)를 저장한다. (이를 통해 사용자의 인증 및 권한은 다음 요청을 위해 보존된다.)
5. 최종적으로 요청을 처리한 후, 서버는 HTTP 응답을 클라이언트에 반환한다.
➡️ SecurityContextPersistenceFilter를 사용하여 SecurityContext를 보존하면, 사용자의 로그인 상태와 보안 정보가 여러 요청 간에 유지되어, 사용자는 로그인 한 후에 동일한 상태 정보를 유지할 수 있게 된다.
🙋♀️ 세션(Session)이란?
웹 애플리케이션에서 상태 정보를 유지하고 관리하기 위한 매커니즘. 브라우저와 서버 간의 히스토리.
🙋♀️ SecurityContext란?
스프링 시큐리티에서 사용자의 보안 정보 및 인증 상태를 저장하는 객체. 사용자가 로그인하면 SecurityContext에 사용자의 인증 정보(authentication)와 권한 정보를 담아 SecurityContextHolder에 저장한다.
위처럼 동작한다면, 리다이렉트된 페이지에서 authentication을 잃어버리는 일은 발생하지 않았을텐데,
위처럼 동작하지 않은 이유가 무엇일까?
위 동작 과정 2번에서 SecurityContextRepository를 사용한다고 했다.
SecurityContextRepository으로 기본적으로 HttpSessionSecurityContextRepository를 사용하는데, `SessionCreationPolicy.STATELESS`를 설정하면 NullSecurityContextRepository를 사용한다.
NullSecurityContextRepository를 사용하면, 세션에 담긴 이전 SecurityContext를 사용하지 않고 빈 SecurityContext를 로드하게 된다.
❗️ 따라서 인증 정보가 요청 간 유지되지 않으므로 모든 요청에서 새로운 인증이 필요하다.
디버깅
프로젝트에서 이를 확인하기 위해 SessionCreationPolicy를 다르게 설정해 디버깅을 해보았다.
`SessionCreationPolicy.STATELESS`를 설정한 경우 - 세션 사용 X
SecurityContextRepository로 NullSecurityContextRepository가 선택되었고, NullSecurityContextRepository의 loadContext() 메서드가 빈 SecurityContext를 반환하는 모습을 확인할 수 있다.
`SessionCreationPolicy.STATELESS`를 설정하지 않은 경우 - 세션 사용 O
SecurityContextRepository로 HttpSessionSecurityContextRepository가 선택되었고, HttpSessionSecurityContextRepository의 loadContext() 메서드가 세션에서 SecurityContext를 가져와서 반환하는 모습을 확인할 수 있다.
문제 해결
결국 `SessionCreationPolicy.STATELESS` 설정을 해제하여 문제를 해결할 수 있었다.
스포티파이 로그인 성공 후, 사용자가 요청한 페이지로 리다이렉트 되었을 때에도 authentication을 유지하고 있는 모습이다. 🎉
참고 자료
SecurityContextPersistenceFilter 알아보기
[스프링 시큐리티] SecurityContext 객체 생성, 저장, 조회(SecurityContextPersistenceFilter)