이번 포스팅에서는 스프링 프레임워크의 하위 프레임워크인 Spring Security의 아키텍쳐에 대해 알아보겠습니다.
이 글을 명확하게 이해하기 위해선 서블릿 컨테이너에 의해 관리되는 디스패쳐 서블릿, 스프링 컨테이너와 디스패쳐 서블릿의 관계, Proxy 패턴에 대한 선수지식이 필요합니다.
Spring Security - Filter
이름에서도 알 수 있듯이 Spring Security는 Spring 어플리케이션에 보안 서비스를 제공하는 프레임워크 입니다.
Spring Security가 제 기능을 하려면 네트워크를 통해 스프링 어플리케이션에 접근하는 모든 request들에 대해 적용되어야 하므로 이는 모든 네트워크 요청을 수신하고 그에 대해 응답하는 Dispatcher Servlet에 대하여 이루어 져야 합니다. 따라서, Spring Security는 서블릿 컨테이너의 디스패쳐에 일련의 레이어를 씌움으로써 스프링 프레임워크에 보안 서비스를 제공합니다.
스프링 시큐리티는 이 레이어를 Filter(필터)라고 부릅니다.
웹 요청을 받은 서블릿 컨테이너는 url를 확인하여 이 url에 적용할 적절한 FilterChain(필터체인)을 생성합니다. 필터체인은 내부적으로 필터 인스턴스와 디스패쳐 서블릿 인스턴스를 보유하며 일련의 순서로 체이닝 되어 실행됩니다. ( Spring MVC를 사용하는 경우 FilterChain은 서블릿 인스턴스로 디스패쳐 서블릿 인스턴스를 가집니다 ) FilterChain은 서블릿 컨테이너에 의해 실행되며 이에 따라 요청이 디스패쳐 서블릿으로 도착하기 전에 Filter 들이 동작하도록록 합니다. 이 인스턴스들은 아직 디스패쳐 서블릿에 의해 처리되지 않은 요청이므로, HttpServletRequest과 HttpServletResponse 객체를 파라미터로 가지고, 이에 대해 필터링을 수행합니다.
FilterChain이 보유한 Filter, Servlet 인스턴스는 순서를 가짐으로 Filter들은 협력하여 아래와 같은 기능을 제공할 수 있습니다.
1. Web Request에 의해 다운스트림(자신보다 후에 실행될) 필터들, 서블릿이 호출되는것을 막을 수 있습니다.
-> 이 경우, HttpServletResponse에 대해 적절한 작업을 수행합니다.
2. 다운스트림의 필터가 사용할 수 있도록 HttpServletRequest, HttpServletResponse를 상황에 맞게 modify할 수 있습니다.
스프링 시큐리티는 서블릿 컨테이너에 필터들을 등록 함으로써 웹 요청이 디스패쳐 서블릿에 도달하기 전에 이를 가로챕니다.
가로챈 요청에 대해 필터를 적용하며, 이를 모두 통과 했을 시, 웹 요청이 디스패쳐 서블릿에 도달하게 됩니다.
요청이 디스패쳐 서블릿에 도달하면 디스패쳐 서블릿에 의해 적절한 컨트롤러로 매핑되게 됩니다.
Spring Security - DelegatingFilterProxy
서블릿 컨테이너에 웹 요청이 도착하면, 서블릿 컨테이너는 URI를 바탕으로 적절한 FilterChain을 생성하여 필터링을 수행하고, 요청이 이를 무사히 통과했을때, 디스패쳐 서블릿에 이 요청이 전달됩니다. 그렇다면, 스프링 어플리케이션 개발자가 임의로 필터를 추가하기 위해선 서블릿 컨테이너(즉, 스프링 컨테이너 외부)에 필터를 추가해야 할까요? 이는 스프링의 Containerless 지향점에 어긋난다고 생각이 들겁니다.
서블릿 컨테이너는 이 필터들을 등록할 수 있도록 필터를 생성할 수 있는 API를 제공하기는 하지만, 스프링 컨테이너 외부에 대해 개발자가 작업해야 하므로 이는 스프링 컨테이너 내에서 관리되는 빈들을 직접 등록하지 못한다는 단점이 있습니다.
이를 해결하기 위해 Spring Security는 서블릿 컨테이너에 미리 Filter에 프록시 패턴을 적용한 필터를 등록하고 이 필터가 빈에 대한 인스턴스를 가지도록 구현했습니다. 즉, 빈을 서블릿 컨테이너의 필터로 등록 할 수 있도록 구현했습니다. 이렇게 빈을 등록할 수 있도록 구현한 Filter를 DelegatingFilterProxty라고 합니다.
DelegatingFilterProxy는 이렇게 서블릿 컨테이너와 스프링 컨테이너의 연결을 제공하여 스프링 어플리케이션 개발자가 스프링 컨테이너 외부에 대한 작업을 할 필요가 없도록, 스프링의 Containerless를 지켜냈습니다.
DelegatingFilterProxy는 여전히 서블릿 컨테이너가 제공하는 필터 등록 과정에 따라 등록되야 합니다. 하지만, 이는 스프링 컨테이너가 자동으로 수행하며, 이렇게 등록된 DelegatingFilterProxy가 실제로 제공할 필터링 기능은 스프링 컨테이너에 등록될 빈이 구현하도록 합니다.
이렇게 우리는 빈을 디스패쳐 서블릿 외부(즉, 스프링 컨테이너 외부)에서 사용할 수 있도록 구현했지만, 빈을 하나(즉, 하나의 필터)만 등록할 수 있다는 단점이 존재 합니다.
Spring Security - FilterChainProxy
Spring Security는 위의 문제를 프록시 패턴을 이용함으로써 해결했습니다. DelegatingFilterProxy가 사용할 빈을 FilterChainProxy 빈 으로 등록하고 FilterChainProxy빈이 필터 역할을 수행하는 인스턴스들을 가지도록 하도록 하여 여러 필터를 빈으로 등록할 수 있도록 구현했습니다. 여러 필터들을 또한 일련의 순서를 가지고 필터작업을 수행해야 합니다. 따라서, Filter 빈들을 묶어 하나의 FilterChain을 구성하며, 이렇게 스프링 컨테이너 내에서 개발할 수 있게된 FilterChain을 SecurityFilterChain이라고 칭합니다
Spring Security - SecurityFilterChain
SecurityFilterChain은 스프링 컨테이너에서 구성한 필터, SecurityFilter들을 가집니다. SecurityFilterChain은 FilterChainProxy가 받은 요청에 대해 실행할 FilterChian들을 제공합니다.
SecurityFilterChain은 여러개가 존재할 수 있습니다. 임의의 Web 요청에 따라 어떤 SecurityFilterChain이 실행될지는 FilterChainProxy 이 결정하도록 합니다. 빈은 아래와 같이 여러개의 SecurityFilterChain을 가지고 있습니다.
요청에 대해 FilterChainProxy는 만족하는 SecurityFilterChain을 찾습니다. 적합한 SecutiyFilterChain을 찾으면 해당 SecurityFilterChain을 실행하고 다른 SecurityFilterChain들은 실행되지 않습니다. 따라서, 어떤 SecurityFilterChain은 실행되지 않을 수도 있습니다.
이렇게 FilterChainProxy를 통해 스프링 컨테이너에서 정의한 SpringSecurityChain을 사용함으로써 얻게 되는 세가지 이점이 있습니다.
1. 개발자가 정의한 모든 Security관련 서비스는 모두 FilterChainProxy 빈 에서 시작되게 됩니다.
이를통해 디버깅 시 디버깅 포인트를 FIlterChainProxy에 지정하면되는 간편함이 있습니다.
2. 모든 요청에 대해 필수적으로 적용되야 하는 변경사항을 간편하게 적용할 수 있습니다.
FilterChainProxy빈이 모든 Spring Security 서비스의 시작점이자 중심이기 때문입니다. 예를들면 방화벽 설정, 혹은 연결 거부 등등
3. 임의의 SecurityFilterChain이 요청에 대해 실행될 시점을 더 유연하게 지정할 수 있습니다.
기존의 서블릿 컨테이너의 필터들은 모든 요청들에 대해 URL만을 확인하여 필터의 실행시점을 정할 수 있었습니다. 하지만, FilterChainProxy의 RequestMatcher를 사용함으로써 HttpServletRequest의 모든 정보를 활용하여 임의의 SecurityFilterChain을 실행할 수 있게 됐습니다.
Spring Security - Security Filters ( SecurityFilterChain 구성하기 )
임의의 SecurityFilter를 등록하는 방법은 간단합니다.
Spring Security는 자동으로 FilterChainProxy에 등록해주는 API를 제공하므로 SecurityFilterChain을 빈으로 등록해주기만 하면 이는 자동으로 FilterChainProxy에 등록되게 됩니다.
아래 예제에서는 두가지 SecurityFilterChain을 등록했습니다.
아래는 실행 결과입니다. SpringSecurity가 기본적으로 제공되는 여러 필터들이 적용되어있는 것을 확인할 수 있으며, 설정한 두가지 SecurityFilterChain이 성공적으로 등록된것을 확인할 수 있습니다.
[DefaultSecurityFilterChain
[RequestMatcher=any request,
Filters=
[org.springframework.security.web.session.DisableEncodeUrlFilter@438a2d3, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@6fe9c048, org.springframework.security.web.context.SecurityContextHolderFilter@5740ad76, org.springframework.security.web.header.HeaderWriterFilter@43ca96a0, org.springframework.web.filter.CorsFilter@54d46c8, org.springframework.security.web.csrf.CsrfFilter@3980b44f, org.springframework.security.web.authentication.logout.LogoutFilter@564d3940, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@79c849c7, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@35840ecc, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@1ddba7a0, org.springframework.security.web.access.ExceptionTranslationFilter@2b506a79, org.springframework.security.web.access.intercept.AuthorizationFilter@c7cf8c4]]
, DefaultSecurityFilterChain
[RequestMatcher=any request,
Filters=
[org.springframework.security.web.session.DisableEncodeUrlFilter@3cc817bd, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@64b8eb96, org.springframework.security.web.context.SecurityContextHolderFilter@5f32ab17, org.springframework.security.web.header.HeaderWriterFilter@4ae6451d, org.springframework.web.filter.CorsFilter@776a7ec6, org.springframework.security.web.csrf.CsrfFilter@67cd84f9, org.springframework.security.web.authentication.logout.LogoutFilter@70e54ec3, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@54530644, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@42734b71, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@2d23a5be, org.springframework.security.web.access.ExceptionTranslationFilter@52b891de]]]
위 로그를 보면 RequestMatcher가 등록되지 않아 둘중 요청에 먼저 매칭되는 SecurityFilterChain이 실행될것을 짐작해 볼 수 있습니다.
후의 포스팅에서 RequestMatcher를 구성하는 방법을 알아보겠습니다.
Spring Security - Custom Filter to Filter Chain
이제 SecurityFilterChain을 빈을 통해 구성하는 방법을 알았으므로, 커스텀 필터를 등록하는 방법을 알아보겠습니다.
스프링 컨테이너에서 필터를 임의로 구성하기 위해서는 Filter 인터페이스, 혹은 OncePerRequestFilter를 구현해야 합니다.
따라서 순서는 다음과 같습니다.
1. Filter를 구현하는 클래스를 작성한다.
2. 등록될 FIlterChain에 해당 클래스를 추가한다.
## 필터를 구현하는 클래스 해당 예제에서는 이를 빈으로 구성하지 않는다
public class TenantFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String tenantId = request.getHeader("X-Tenant-Id");
boolean hasAccess = isUserAllowed(tenantId);
if (hasAccess) {
filterChain.doFilter(request, response);
return;
}
throw new AccessDeniedException("Access denied");
}
}
Filter를 빈으로 구성할시 스프링 컨테이너는 자동적으로 이를 서블릿 컨테이너에 등록하게 됩니다. 이는 해당 필터가 요청에 대해 두번 호출되게 되므로 이에 대해 주의하여 구성해야 합니다. 하지만, 스프링의 이점을 활용하고자 빈으로 구성하고 싶다면 서블릿 컨테이너에 자동으로 등록되지 않도록 아래와 같은 빈을 등록해야 합니다.
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
다음은 구성한 필터를 SecurityFilterChain에 등록하는 방법입니다.
## 자동으로 FilterChainProxy에 등록되기 위해 빈으로 구성한다.
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 임의의 필터
.addFilterBefore(new TenantFilter(), AuthorizationFilter.class); //이를 통해 필터를 구성한다. 내가 구성한 필터가 AuthorizationFilter.class 필터 이전에 실행되도록 구현합니다.
// 임의의 필터
return http.build();
}
Spring Security는 기본적으로 적용하도록 하는 몇가지 필터가 있습니다. AuthorizationFilter 또한 그중 하나이며, 커스텀한 필터를 필터를 넣기 위해서는 반드시 순서를 정의해야 합니다. 따라서 위의 코드처럼 커스텀한 필터가 AuthorizationFilter 이전에 실행되도록 구현했습니다.
Spring Security - Handling Security Exceptions
ExeptionTranslationFilter는 AccessDeniedException 과 AuthenticationException에러를 HTTP Response로 변환할 수 있습니다. ExceptionTranslationFilter는 SecurityFilterChain의 구성으로 FilterChainProxy에 등록됩니다.
1. SecurityFilterChain에 등록된 ExcpetionTranslationFliter가 이후의 필터들을 실행합니다.
2. 필터들이 실행중에 과 AuthenticationException에러를 발생시키면 이를 SecurityTranslationFilter가 처리를 시작합니다.
- (1) SecurityContextHolder가 초기화 됩니다. -> 왜? 후에 Security Authentication 알아보면서 다시하자
- (2) HttpServletRequest를 저장합니다. 후에 Authentication이 성공적으로 완료되면, 이를 이용해 멈췄던 필터부터 이 요청이 처리되도록 합니다.
- (3) AuthenticationEntryPoint는 클라이언트가 자격요을 하는데 사용됩니다. ( OAuth2, 혹은 개인 로그인 서비스 )
3. 만약 AccessDeniedException을 감지했다면 이 요청을 AccessDeniedHandler를 호출하고 이에 대한 처리를 위임합니다.
이렇게 Sprint Security 아키텍쳐에 대한 전반적인 구성을 이해헀습니다. 긴 글 읽어주셔서 감사합니다.
https://docs.spring.io/spring-security/reference/servlet/architecture.html#servlet-logging
'Spring > SpringBoot' 카테고리의 다른 글
[SpringBoot] 스프링의 에러처리 탐구 (0) | 2024.01.17 |
---|---|
[Spring] Spring Security 인증 구성 (0) | 2024.01.02 |
[Spring] 다수의 SecurityFilterChain 구성 방법 (0) | 2024.01.01 |
[Spring] RestTemplate (1) | 2023.12.29 |
SpringBoot의 작동원리를 직접 구현 해보며 이해하자(1) - Servlet (0) | 2023.12.22 |