Spring Security에서 인증이 완료된 Authentication(인증 정보)를 SecurityContextHolder에 저장하는 과정에 대해 정리한 내용입니다.
인증이 진행되는 과정 및 구현 내용이 정리된 포스팅입니다. 인증을 통해 Authentication을 반환하는 과정이 담겨있기 때문에 동작 원리를 파악하기 위해서는 먼저 보고 오셔도 좋을 것 같습니다.
(WebSecurityConfigurerAdapter가 deprecated 되기 전이라 해당 부분은 적용이 안 되어 있지만 흐름을 참고하시기에는 괜찮을 것 같습니다.)
- Authentication, SecurityContext, SecurityContextHolder 개념과 구조
접근 주체의 정보와 권한을 담는 Authentication
접근 주체의 인증 정보와 권한을 담는 인터페이스입니다. 스프링 시큐리티에서는 인증 시 id와 password를 이용한 credential 기반의 인증을 사용하며, 인증 후 최종 인증 결과를 담아 SecurityContext에 보관되고 필요할 때 전역적으로 참조가 가능합니다.
Authentication 구조
- principal : 접근 주체의 아이디 혹은 User 객체를 저장합니다.
- credentials : 접근 주체의 비밀번호를 저장합니다.
- authorities : 인증된 접근 주체자의 권한 목록을 저장합니다.
- details : 인증에 대한 부가 정보를 저장합니다.
- authenticated : boolean 타입의 인증 여부를 저장합니다.
SecurityContext
Authentication 객체가 보관되는 저장소 역할을 합니다. 필요시 SecurityContext로부터 Authentication 객체를 꺼내서 사용할 수 있습니다.
각 스레드마다 할당되는 고유 공간인 ThreadLocal에 저장되기 때문에 동일한 스레드인 경우 필요한 아무 곳에서나 참조가 가능합니다.
다시 말하면, 서버에 접속해서 생성되는 각 Thread는 각각의 ThreadLocal에 SecurityContext를 가지고 있는 것입니다.
(SecurityContext는 ThreadLocal에 저장되어 있으면서 동시에 HttpSession에도 저장되어 있습니다.)
SecurityContextHolder
SecurityContext 객체를 보관하고 있는 wrapper 클래스입니다. SecurityContextHolder에서는 ThreadLocal의 전략을 설정할 수 있는데요.
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
public static final String MODE_GLOBAL = "MODE_GLOBAL";
(SecurityContextHolder에 정의된 ThreadLocal 전략을 설정하기 위한 static 변수)
SecurityContextHolder 클래스의 initializeStrategy() method입니다.
ThreadLocal의 전략에는 'MODE_THREADLOCAL', 'MODE_INHERITABLETHREADLOCAL', 'MODE_GLOBAL' 세 가지 전략이 있는데요.
- MODE_THREADLOCAL : default 설정 값이며, 스레드당 SecurityContext 객체를 할당합니다.
- MODE_INHERITABLETHREADLOCAL : 하위로 생성된 Thread에서 동일한 SecurityContext 객체를 공유하게 됩니다.
- MODE_GLOBAL : 애플리케이션의 모든 Thread가 단 하나의 SecurityContext만을 공유하게 됩니다.
(각 모드에 따라 SecurityContextHolderStrategy의 구현체가 선택되고, 해당 구현체가 실제로 SecurityContext를 저장하고, 가지고 오는 역할을 합니다.)
- SecurityFilterChain
Spring Security는 표준 서블릿 필터를 기반으로 동작하는데요. SpringBoot의 기본 설정을 사용하는 경우, 인증에 사용되는 Filter들이 모여있는 SecurityFilterChain을 자동으로 등록해주는데, 이 SecurityFilterChain을 통해 스프링 시큐리티의 인증 과정이 동작하게 됩니다.
(동작에 있어 여러 개의 Filter들이 체인처럼 연결되어 있어서 Chain이라는 표현을 사용합니다.)
***
이때 FilterChainProxy라는 클래스가 등장하는데, 해당 클래스는 DelegatingFilterProxy로부터 Filter 작동에 대한 요청을 위임받아 실제로 인증 처리를 하는 클래스입니다.
Spring Security를 사용하면 모든 요청은 FilterChainProxy 클래스를 거치게 되며, 내부에 getFilters() 메서드를 통해 SecurityFilterChain의 Filter 목록을 가져오게 됩니다.
(SecurityFilterChain의 구조)
SecurityContextPersistenceFilter
그중 핵심이 되는 SecurityContextPersistenceFilter에 대해서 살펴보면, 해당 필터는 이름에서 추측해볼 수 있는 것처럼 SecurityContext 객체의 생성, 조회, 저장 등의 LifeCycle을 담당하는 필터입니다.
클래스 내부를 살펴보면, 실제 SecurityContext 객체를 생성, 조회, 저장하는 것은 내부에 SecurityContextRepository가 담당하고 있는데요. loadContext() method, saveContext() method, containsContext() method
SecurityContextRepository는 인터페이스이며, 일반적으로 HttpSessionSecurityContextRepository가 구현체로 사용되고 있습니다.
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// ensure that filter is only applied once per request
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
if (this.forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (this.logger.isDebugEnabled() && session.isNew()) {
this.logger.debug(LogMessage.format("Created session %s eagerly", session.getId()));
}
}
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
try {
SecurityContextHolder.setContext(contextBeforeChainExecution);
if (contextBeforeChainExecution.getAuthentication() == null) {
logger.debug("Set SecurityContextHolder to empty SecurityContext");
}
else {
if (this.logger.isDebugEnabled()) {
this.logger
.debug(LogMessage.format("Set SecurityContextHolder to %s", contextBeforeChainExecution));
}
}
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
// Crucial removal of SecurityContextHolder contents before anything else.
SecurityContextHolder.clearContext();
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
this.logger.debug("Cleared SecurityContextHolder to complete request");
}
}
SecurityContextPersistenceFilter의 doFilter() method
1. 익명의 주체가 접근하는 경우
해당되는 경우는 AnonymousAuthenticationFilter가 동작하게 되는데요. 해당 필터에서 AnonymousAuthenticationToken 토큰을 통해 Authentication 객체를 만들어 SecurityContext에 저장하게 됩니다.
2. 인증하기 위한 주체가 접근하는 경우
loadContext() method 동작 과정에서 securityContext 객체가 없는 상태이기 때문에 새로운 SecurityContext 객체를 생성하여 SecurityContextHolder에 저장하게 됩니다.
이어서 chain.doFilter() 메서드를 타고 다음 UsernamePasswordAuthenticationFilter로 넘어가게 되는데, 해당 필터에서 인증 성공 후 생성되는 Authentication 객체를 SecurityContext에 저장하게 됩니다.
3. 인증된 주체가 접근하는 경우
loadContext() method를 통해 HttpSession에서 SecurityContext 객체를 꺼내와서 SecurityContextHolder에 저장합니다.
SecurityContext 객체 안에는 인증 주체에 대한 정보 및 권한 정보를 가진 Authentication 객체가 존재하기 때문에 계속 인증을 유지할 수 있습니다.
*** finally
finally 부분의 로직 중 SecurityContextHolder.clearContext() method에 주목해보면, 해당 메서드는 ThreadLocal에 있는 SecurityContext를 제거하는 역할을 하는데요.
서블릿 기반의 웹 프로그램은 매 요청마다 스레드를 생성하게 되는데, 스레드마다 생겨나는 SecurityContext로 인한 메모리의 누수를 막기 위한 로직입니다.
(내용 중 잘못된 부분은 지적해주시면 다시 확인하고 수정하도록 하겠습니다 감사합니다.)
< 참고 자료 >
'Programming > Spring Boot' 카테고리의 다른 글
ExceptionTranslationFilter과 SecurityInterceptor (0) | 2022.08.01 |
---|---|
세션을 사용한 스프링 시큐리티 구현(WebSecurityConfigurerAdapter deprecated) (0) | 2022.07.21 |
@Valid @Validated 동작 원리 파헤치기 (4) | 2022.07.01 |
(Spring Boot) Custom Validator 적용하는 방법, 단일 및 다중 필드 (3) | 2022.06.29 |
Spring swagger 3 사용방법(springdoc-openapi-ui) (2) | 2022.06.25 |