
해당 글은 Spring Security 공식 문서인 https://docs.spring.io/spring-security 를 기반으로 작성된 글입니다.
아래 글들은 이 글의 이해를 기반으로 설명합니다.
단일 HTTP 요청에 대한 핸들러의 일반적인 계층화

혹여나 사진이 잘 안보인다면 다크 모드를 해제하는 것을 추천한다.
Spring MVC Application에서 the Servlet is an instance of DispatcherServlet.
하나의 서블릿은 최대 하나의 HttpServletRequest, HttpServletResponse를 처리할 수 있지만 둘 이상의 필터를 사용할 수 있다.
Downstream Filter instance 또는 Servlet이 호출되는 것을 방지한다.
이 경우 필터는 일반적으로 HttpServletResponse를 작성합니다.
Downstream Filter instance 및 Servlet에서 사용하는 HttpServletRequest 또는 HttpServletResponse를 수정한다.
Filter의 힘은 FilterChain에서 나온다.
FilterChain 사용 예시
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// do something before the rest of the application
chain.doFilter(request, response); // invoke the rest of the application
// do something after the rest of the application
}
필터는 하류 필터 인스턴스와 서블릿(Servlet)에만 영향을 미치기 때문에 각 필터가 호출되는 순서는 매우 중요하다.
여기서 말하는 "하류 필터 인스턴스와 서블릿(Servlet)"는 FilterChain에서 현재 필터의 위치보다 아래쪽에 있는 필터와 서블릿을 의미한다. 필터는 체인 형태로 연결되어 요청을 처리하는데, 하류에 위치한 필터와 서블릿에만 영향을 미치게 된다.
따라서 각 필터가 호출되는 순서는 요청 또는 응답에 대한 처리 과정에 큰 영향을 미치게 된다. 필터의 순서를 정확히 관리함으로써 원하는 동작을 수행할 수 있고, 응답을 수정하거나 요청을 보호하는 등의 작업을 올바르게 수행할 수 있다.
Spring은 DelegatingFilterProxy 필터 구현을 제공한다.
이 필터는 Servlet container의 lifecycle과 Spring ApplicaitonContext 사이의 연결을 가능하게 한다.
DelegatingFilterProxy는 서블릿 필터(Servlet Filter)를 Spring의 빈으로 등록하여 Spring의 ApplicationContext에 의해 관리될 수 있도록 해준다. 이를 통해 Spring의 빈 생명주기와 서블릿 컨테이너의 라이프사이클을 통합할 수 있다. 이 필터는 일반적으로 Spring Security와 같이 Spring 애플리케이션에서 서블릿 필터를 사용할 때 유용하게 사용된다.
DelegatingFilterProxy를 사용하면 Spring의 IoC 기능을 활용하여 필터를 구성하고 의존성을 주입할 수 있으며, 서블릿 컨테이너의 라이프사이클에 따라 필터를 초기화하고 소멸할 수 있다. 이를 통해 Spring 애플리케이션에서 서블릿 필터를 쉽게 사용할 수 있고, Spring의 다양한 기능과의 통합이 용이해진다.

DelegatingFilterProxy는 ApplicationContext에서 위 사진 속 하얀색 Bean Filter를 조회한 다음 Bean Filter를 호출한다.
DelegatingFilterProxy Pseudo Code
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
Filter delegate = getFilterBean(someBeanName); //1
delegate.doFilter(request, response); //2
}
delegate는 Bean Filter의 인스턴스이다.DelegatingFilterProxy의 또 다른 장점은 필터 빈 인스턴스 조회를 지연시킬 수 있다는 것이다.
이는 컨테이터가 시작되기 전에 필터 인스턴스를 등록해야 하기 때문에 중요하다.
그러나 Spring은 일반적으로 ContextLoaderListener를 사용하여 SPring Bean을 로드하는데, 이 작업은 Filter 인스턴스를 등록한 후에야 수행된다.
FilterChainProxy의 역할

SecurityFilterChain은 현재 요청에 대해 어떤 Spring Security Filter instance를 호출해야 하는지 결정하기 위해 FilterChainProxy에서 사용된다.
SecurityFilterChain의 역할

SecurityFilterChain의 보안 필터는 일반적으로 Bean이지만, DelegatingFilterProxy가 아닌 FilterChainProxy에 등록된다.
서블릿 컨테이너에서 필터 인스턴스는 URL 기반으로 호출된다. 그러나 FilterChainProxy는 RequestMatcher 인터페이스를 사용하여 HttpServletRequest의 모든 것을 기반으로 호출을 결정할 수 있다.
여러 SecurityFilterChain 인스턴스

위 그림에서 FilterChainProxy는 어떤 SecurityFilterChain을 사용할지 결정한다.
이때 조건이 일치하는 SecurityFilterChain만 호출된다.
만약 api/messages/ URL이 요청되면 먼저 /api/**의 SecurityFilterChain 패턴과 일치하므로 이를 호출한다.
만약 일치하는 SecurityFilterChain이 없다면 /**의 SecurityFilterChain이 호출된다.
Security Filter는 SecurityFilterChain API를 통해 FilterChainProxy에 삽입된다.
이러한 필터는
등 다양한 용도로 사용할 수 있다.
필터는 특정 순서로 실행되어 적시에 호출되도록 보장한다.
일반적으로 Spring Security의 필터 순서를 알 필요는 없다. 그러나 순서를 아는 것이 도움이 되는 경우가 있는데 알고 싶다면 FilterOrderRegistration code를 확인해보자.
Spring Configuration를 고려해보자
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(Customizer.withDefaults())
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults());
return http.build();
}
}
위의 코드는 다음과 같은 필터 순서를 생성한다.
대부분의 경우 기본 보안 필터만으로도 애플리케이션에 보안을 제공할 수 있다, 하지만 보안 필터 체인에 사용자 지정 필터를 추가하고 싶다면 어떻게 할까?
예를 들어 tenantId 헤더를 가져오는 필터를 추가하여 현재 사용자가 해당 tenant에 액세스할 수 있는지 확인하고자 한다고 가정해보자.
이전 설명에서 이미 필터를 추가할 위치에 대한 단서가 나왔다. 현재 사용자를 알아야 하므로 인증 필터 다음에 필터를 추가해야 한다.
먼저 사용자 지정 필터를 만들어보자
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"); // 1
boolean hasAccess = isUserAllowed(tenantId); // 2
if (hasAccess) {
filterChain.doFilter(request, response); // 3
return;
}
throw new AccessDeniedException("Access denied"); // 4
}
}
위의 코드는 다음을 수행한다.
1. 요청 헤더에서 tenantId를 가져온다.
2. 현재 사용자에게 tenantId에 대한 액세스 권한이 있는지 확인한다.
3. 사용자에게 액세스 권한이 있으면 체인의 나머지 필터를 호출한다.
4. 사용자에게 액세스 권한이 없으면 AccessDeniedException을 던진다.
이제 사용자 지정 필터를 만들었으니 SecurityFilterChain에 필터를 추가하자.
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.addFilterBefore(new TenantFilter(), AuthorizationFilter.class);
return http.build();
}
AuthoricationFilter 앞에 필터를 추가하면 인증 필터 다음에 TenantFilter가 호출되도록 할 수 있다.
특정 필터 뒤에 필터를 추가하려면 addFilterAfter를 사용하거나 필터 체인의 특정 필터 위치에 필터를 추가하려면 addFilterAt을 사용할 수도 있다.
ExceptionTraslationFilter를 사용하면 AccessDeniedException과 AuthenticationException을 HTTP 응답으로 번역할 수 있다.
ExceptionTraslationFilter는 FilterChainProxy 중 하나로 삽입된다.
ExceptionTraslationFilter와 다른 컴포넌트와의 관계

1. 먼저 예외 번역 필터가 FilterChain.doFilter(request, response)를 호출하여 애플리케이션의 나머지 부분을 호출한다.
2. 사용자가 인증되지 않았거나 인증 예외인 경우 인증을 시작한다.
ExceptionTranslationFilter pseudocode
try {
filterChain.doFilter(request, response); //1
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication(); //2
} else {
accessDenied(); //3
}
}
보안 예외 처리에서 설명한 것처럼 요청에 인증이 없고 인증이 필요한 이소스에 대한 요청인 경우 인증이 성공한 후 다시 요청할 수 있도록 인증된 리소스에 대한 요청을 저장해야 한다.
Spring Security에서는 요청 캐시 구현을 사용하여 HttpServletRequest를 저장하여 이 작업을 수행한다.
HttpServletRequest는 RequestCache에 저장된다.
사용자가 성공적으로 인증하면 요청 캐시는 원래 요청을 실행하는데 사용된다.
RequestCacheAwareFilter
기본적으로 HttpSessionRequestCache가 사용된다. 아래 코드는 'continue'라는 매개변수가 있는 경우 저장된 요청에 대해 HttpSession을 확인하는 데 사용되는 RequestCache 구현을 사용자 정의하는 방법을 보여준다.
'continue' 매개변수가 있는 경우에만 저장된 요청을 확인하는 RequestCache
@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
requestCache.setMatchingRequestParameterName("continue");
http
// ...
.requestCache((cache) -> cache
.requestCache(requestCache)
);
return http.build();
}
Prevent the Request From Being Saved
사용자의 미인증 요청을 세션에 저장하지 않으려는 이유는 여러 가지가 있다.
이를 위해 NullRequestCache를 구현할 수 있다.
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
RequestCache nullRequestCache = new NullRequestCache();
http
// ...
.requestCache((cache) -> cache
.requestCache(nullRequestCache)
);
return http.build();
}
RequestCacheAwareFilter는 RequestCache를 사용하여 HttpServletRequest를 저장한다.
Spring Security는 모든 보안 관련 이벤트에 대한 포괄적인 Logging을 DEBUG 및 TRACE 수준에서 제공한다.
이는 애플리케이션을 디버깅할 때 매우 유용할 수 있는데, 보안을 위해 Spring Security는 요청이 거부된 이유에 대한 세부 정보를 본문에 추가하지 않기 때문이다.
사용자가 CSRF 토큰 없이 CSRF 보호가 활성화된 리소스에 POST 요청을 시도하는 경우를 예로 들어보자. 로그가 없으면 사용자에게 요청이 거부된 이유에 대한 설명 없이 403 오류가 표시된다.
그러나 Spring Security에 대해 Logging을 활성화하면 다음과 같은 로그 메시지가 표시된다.
2023-06-14T09:44:25.797-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Securing POST /hello
2023-06-14T09:44:25.797-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/15)
2023-06-14T09:44:25.798-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/15)
2023-06-14T09:44:25.800-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderFilter (3/15)
2023-06-14T09:44:25.801-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/15)
2023-06-14T09:44:25.802-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking CsrfFilter (5/15)
2023-06-14T09:44:25.814-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:8080/hello
2023-06-14T09:44:25.814-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.s.w.access.AccessDeniedHandlerImpl : Responding with 403 status code
2023-06-14T09:44:25.814-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match request to [Is Secure]
CSRF 토큰이 누락되어 요청이 거부되는 것을 알 수 있다.
모든 보안 이벤트를 기록하도록 애플리케이션을 구성하려면 애플리케이션에 다음의 설정을 추가하면 된다.
application.properties
logging.level.org.springframework.security=TRACE
logback.xml
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- ... -->
</appender>
<!-- ... -->
<logger name="org.springframework.security" level="trace" additivity="false">
<appender-ref ref="Console" />
</logger>
</configuration>
출처
https://docs.spring.io/spring-security/reference/servlet/architecture.html