02. SecurityFilterChain
- -
이번 포스트에서는 Servlet Application에서 동작하는 Spring Security의 기본 아키텍쳐에 대해서 살펴보자.
Filter
Filter와 DelegatingFilterProxy
Servlet 기반의 Application에서 동작하는 Spring Security는 filter에서부터 시작한다. Filter는 Container가 요청을 접수한 후 어떤한 Servlet 요청을 처리하기 전에 동작하는 웹 컴포넌트이다. 이 filter를 통해서 Servlet에서 필요한 전/후 처리를 모듈화할 수 있다.
이 필터는 하나만 존재하는 것은 아니고 목적에 따라 여러가지가 존재한다. 쉽게 생각하면 encoding, logging, session 관리를 위한 필터등을 예로 들수 있다.
이 필터들은 따로 따로 동작하지 않고 연결되서 동작한다. 공기청정기가 공기를 깨끗하게 하기 위해 여러 필터를 거치는 것과 같은 이치이다.
이처럼 여러개의 필터가 연결해서 동작하는 것을 filter chain이라고 한다.
filter chain | DelegatingFilterProxy |
출처: https://docs.spring.io/spring-security/reference/servlet/architecture.html |
이 Filter는 Spring과 무관한 기술이다. 따라서 Spring Framework의 다양한 빈들을 사용하는게 기본적으로 말이 안된다. 이에 Spring에서는 DelegatingFilterProxy라는 이름의 filter를 제공한다.
DelegatingFilterProxy는 Servlet 컨테이너의 라이프사이클과 Spring의 ApplicationContext 사이의 브릿지 역할을 수행한다. 기본 동작은 Spring의 ApplicationContext에서 @Bean으로 선언된 Filter의 목록을 가져와서 실행시켜준다.
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
Filter delegate = getFilterBean(someBeanName); // @Bean으로 등록된 필터를 가져온다.
delegate.doFilter(request, response);
}
DelegatingFilterProxy는 getFilterBean() 메서드로 빈을 가져오면서 Lazy 하게 가져온다. 왜냐면 일반적으로 Spring은 ContextLoaderListener를 사용해서 Spring Bean을 로드하는데 이 작업은 Filter 등록 후이기 때문이다.
FilterChainProxy과 SecurityFilterChain
Spring Security에서 추가로 FilterChainProxy라는 특별한 필터를 제공한다. FilterChainProxy는 빈이면서 Filter인데 주요 역할은 SecurityFilterChain을 통해서 많은 Filter 객체로 처리를 위임한다. FilterChainProxy는 DelegatingFilterProxy로 래핑된다. (Filter로 등록되는 것은 DelegatingFilterProxy인데 동작하는 것은 FilterChainProxy)
FilterChainProxy | SecurityFilterChain |
출처: https://docs.spring.io/spring-security/reference/servlet/architecture.html |
FilterChainProxy는 모든 보안 필터들을 관리하고 조율하는 역할을 수행한다. 우리가 만드는 SecurityFilter들은 직접 Servlet Container에 등록되지 않고 FilterChainProxy가 사용하는 SecurityFilterChain에 의해 FilterChainProxy를 통해서 관리한다.
// FilterChainProxy의 생성자: SecurityFilterChaing을 받는다.
public FilterChainProxy(SecurityFilterChain chain) {
this(Arrays.asList(chain));
}
public FilterChainProxy(List<SecurityFilterChain> filterChains) {
this.filterChains = filterChains;
}
복잡하게 FilterChainProxy를 사용하는 이유는 크게 4가지로 들 수 있다.
- 디버깅이 쉬워진다. 모든 보안 관련 처리가 FilterChainProxy를 통과하므로 여기에서 디버깅 하면 보안 관련 문제를 쉽게 추적할 수 있다.
- 메모리 관리가 편해진다. 현재 로그인한 사용자 정보를 SecurityContext에 담아서 ThreadLocal에서 관리하는데 FilterChainProxy는 요청 처리가 끝나면 자동으로 이를 정리한다.
- 보안이 강화된다. HttpFirewall을 적용되어있어 경로조작 시도(/../), 이중 슬래시(//), 세미콜론 주입(;) 등 다양한 보안 공격으로부터 애플리케이션을 보호한다.
- 무엇보다 큰 장점은 다양한 조건에 따라 보안 필터를 적용할 수 있다. 기본적인 Servlet의 filter는 url-mapping 만으로 필터를 적용하지만 FilterChainProxy를 이용하면 url-mapping과 함께 메서드나 IP 등 여러가지 조건을 적용할 수 있다.
SecurityFilterChain은 현재의 요청에 대해 호출해야 하는 Spring의 보안 필터 객체를 결정하기 위해 FilterChainProxy에서 사용된다.
Multiple SecurityFilterChain
FilterChainProxy는 여러개의 SecurityFilterChain을 가지고 있다가 상황에 따라 적절한 SecurityFilterChain을 결정할 수 있는데 순서 기반으로 처음 매칭된 것을 사용한다.
- 만약 /api/messages/로 요청이 오는 경우는 SecurityFilterChain0이 동작한다.
- /messages/로 요청이 오는 경우 SecurityFilterChain0에서 적용되지 않으므로 FilterChainProxy는 다음 SecurityFilterChain을 순서대로 찾아보고 최종적으로 /**에서 처리된다.
다음은 여러개의 SecurityFilterChain을 동작시키는 예이다.
// API endpoint용
@Bean
@Order(1)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http.securityMatcher("/api/**") // API 경로에만 이 설정 적용
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()).
httpBasic(Customizer.withDefaults());
return http.build();
}
// 웹 애플리케이션용 보안 설정
@Bean
@Order(2)
public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.formLogin(form -> form.loginPage("/login").permitAll());
return http.build();
}
- @Order 어노테이션을 사용해 필터 체인의 우선순위 지정
- /api/** 경로와 일반 웹 경로에 대해 다른 보안 규칙 적용
- API 엔드포인트는 HTTP Basic 인증 사용하며 웹 경로는 폼 로그인 방식 사용
- 각 필터 체인은 securityMatcher()로 적용 범위 지정 가능
SecurityFilter
SecurityFilter는 SecurityFilterChain API를 통해 FilterChainProxy에 삽입되는데 사용자 인증(authentication), 권한 부여(authorization), 공격으로 부터의 보호등 다양한 기능을 수행한다. 이러한 필터들은 특정 순서로 실행되서 적시에 호출되도록 보장되어야 한다. 예를 들어 인증필터와 권한 필터가 있을 때 당연히 인증 필터가 먼저 호출되어야 한다.
다음은 일반적으로 SecurityFilterChain에서 사용하는 Filter의 목록이다.
Security filter chain: [
DisableEncodeUrlFilter // URL 인코딩 비활성화 (보안상의 이유로)
WebAsyncManagerIntegrationFilter // 비동기 웹 요청에 대한 SecurityContext 통합 관리
SecurityContextHolderFilter // SecurityContext를 스레드 로컬 저장소에 설정
HeaderWriterFilter // 보안 관련 HTTP 응답 헤더 추가 (X-Frame-Options 등)
CsrfFilter // CSRF(Cross-Site Request Forgery) 공격 방지
LogoutFilter // 로그아웃 처리 및 관련 핸들러 실행
UsernamePasswordAuthenticationFilter // 사용자명/비밀번호 기반 인증 처리
RequestCacheAwareFilter // 인증 전 요청된 URL 캐싱 및 리다이렉트 관리
SecurityContextHolderAwareRequestFilter // HttpServletRequest에 보안 관련 메서드 추가
RememberMeAuthenticationFilter // 'Remember Me' 토큰 기반 자동 로그인 처리
AnonymousAuthenticationFilter // 인증되지 않은 사용자에게 익명 인증 객체 제공
ExceptionTranslationFilter // 보안 관련 예외(인증/인가 실패) 처리
AuthorizationFilter // 최종적인 요청 권한 검증 및 접근 제어
]
SecurityFilter는 대부분 HttpSecurity 인스턴스를 사용해서 선언된다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(Customizer.withDefaults())
.formLogin(Customizer.withDefaults())
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
);
return http.build();
}
}
위의 코드는 4개의 필터를 설정한 예이다.
Filter | 설정 위치 | 역할 |
CsrfFilter | HttpSecurity#csrf | CSRF 공격에 대비 |
UsernamePasswordAuthenticationFilter | HttpSecurity#formLogin | 요청에 대한 인증 처리 |
AuthorizationFilter | HttpSecurity#authorizeHttpRequests | 요청에 대한 권한 처리 |
Security Exception 처리
Security Exception 처리
ExceptionTranslationFilter는 AccessDeniedException과 AuthenticationException을 HTTP 응답으로 변환시켜주는 필터로 FilterChainProxy에 Security Filter로 추가되어있다.
// ExceptionTranslationFilter의 의사 코드
try {
filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException e) {
if (!authenticated || e instanceof AuthenticationException) {
startAuthentication();
} else {
accessDenied();
}
}
- ExceptionTranslationFilter가 FilterChain#doFilter를 호출한다.
- 아직 사용자 인증이 처리되지 않았거나 AuthenticationException이 발생하면 인증 절차를 시작한다.
- 먼저 SecurityContextHolder를 깨끗히 비운다.
- 나중에 인증이 성공하면 원래의 요청을 다시 수행할 수 있도록 HttpServletRequest를 저장한다.
- AuthenticationEntryPoint가 클라이언트로부터의 자격증명을 요청한다. 예를들어 로그인 페이지로 redirect 하거나 WWW-Authenticate 헤더를 보낼 수 있다.
- 그렇지 않고 AccessDeniedException이 발생하면 접근이 거부되고 이를 처리하기 위해 AccessDeniedHandler를 호출한다.
'Spring security > 01.Security' 카테고리의 다른 글
06. 사용자 및 경로 관리 (0) | 2022.11.18 |
---|---|
05. 프로젝트 구성과 초기 동작 (0) | 2020.08.20 |
04. Servlet Authorization Architecture (0) | 2020.07.16 |
03. Servlet Authentication Architecture (0) | 2020.07.15 |
01. Security 개요 (0) | 2020.06.02 |
소중한 공감 감사합니다