Table of contents
- 1. 간단히 알아보는 스프링의 HTTP 통신 과정
- 2. 스프링 시큐리티가 등록되는 위치, FilterChain
- 3. FilterChain의 구조
- 4. 스프링 시큐리티의 시작점, FilterChainProxy
- 5. 요구사항에 따라 보안 전략을 달리 구성하고 싶다면? 다중 SecurityFilterChain
- 6. 동작 구조 요약
- 📚 참고하면 좋은 자료
해당 문서와 관련된 코드는 링크를 참고해주세요.
Spring Security 시리즈는 아래 버전을 기준으로 작성되었습니다.
글을 시작하며
Spring Security에서 인증, 인가 등 보안 관련 기능을 처리하기 위해 내부적인 작동이 어떻게 이뤄질까요?
Servlet 환경을 기준으로 Spring Security가 동작하기 위한 Filtering 매커니즘에 대해 알아보겠습니다.
이해를 돕기 위해, 스프링이 HTTP 요청을 처리하는 시점부터 스프링 시큐리티가 종료되기까지의 과정을 차근차근 살펴보겠습니다.
이 글은 스프링 부트를 기반으로 작성되었으며, 스프링 프레임워크의 일부 세부 사항이 생략되어 있을 수 있습니다.1. 간단히 알아보는 스프링의 HTTP 통신 과정
출처: https://mangkyu.tistory.com/18
스프링은 HTTP 통신을 위해 서블릿이라는 기술을 사용합니다. 서블릿은 웹 애플리케이션 개발 시 HTTP 통신을 보다 쉽게 처리할 수 있게 해주는 기능입니다. 이를 통해 개발자는 HTTP 메세지를 직접 파싱하거나, TCP 소켓을 관리하는 등의 수고를 덜 수 있습니다. 😏
위 사진의 Web Context는 서블릿 영역을 나타내며, 클라이언트 요청을 받으면 DispatcherServlet
이라는 HTTP 서블릿을 이용해 Spring Context로 HTTP 요청을 전달합니다. 이후 Spring Context에서 비즈니스 로직을 처리하여 클라이언트에게 응답을 내려줍니다.
또한, 요청이 Spring Context로 전달되기 전 Filter 레이어가 존재합니다. Filter는 서블릿 영역에 포함되어 HTTP 요청의 전달 전/후로 공통된 작업을 수행할 수 있습니다. 이 글에서 다루게 될 Spring Security가 동작하는 레이어이기도 합니다.
스프링이 HTTP 요청을 처리하는 순서를 요약하면 다음과 같습니다.
- 클라이언트가 HTTP 요청을 서버에 전송합니다.
- 서블릿 컨테이너(Servlet Container)에서 요청을 읽어
HttpServletRequest
,HttpServletResponse
객체를 생성합니다. - Spring Context에 데이터를 전달하기 전, Servlet
FilterChain
을 실행합니다.- FilterChain은 서블릿 영역의 Bean으로 등록된 Filter들을 순차적으로 실행합니다.
- 이후 요청을
DispatcherServlet
으로 전달합니다. 이때부터 Spring Context로 전환됩니다. DispatcherServlet
은 URL 기반 라우팅을 통해 Controller로 요청을 전달합니다.- 비즈니스 로직 수행 후, 생성된 HTTP 응답을 서블릿 컨테이너를 통해 클라이언트로 응답합니다.
알아두면 좋은 내용
- 서블릿과 서블릿 컨테이너는 HTTP 통신을 쉽게 처리하기 위한 스프링의 핵심 요소입니다. 서블릿 컨테이너는 서블릿의 생명주기를 관리하며, HTTP 요청을 받아 서블릿으로 전달합니다.
- 서블릿 컨테이너의 구현체로는 톰캣(Tomcat), 제티(Jetty) 등이 존재하며, TCP 소켓 통신이나 HTTP 메세지를 파싱하여
HttpServletRequest
,HttpServletResponse
객체로 변환하는 과정을 담당합니다. - 서블릿의 구현체는
HttpServlet
이며, 이를 상속한DispatcherServlet
이라는 자바 클래스가 주로 사용됩니다.DispatcherServlet
은 URL 기반 라우팅을 지원하여 요청을 적절한 Controller로 전달합니다. - HTTP 요청이 들어오면 서블릿 컨테이너는 스레드를 생성하고,
2. 스프링 시큐리티가 등록되는 위치, FilterChain
웹 애플리케이션의 안정성을 위해 보안 정책은 HTTP 요청이 컨트롤러로 전달되기 전에 처리되어야 합니다. 이는 보안 위협을 사전에 차단하고, 불필요한 비즈니스 로직 수행을 방지하여 애플리케이션의 효율성과 안정성을 높일 수 있기 때문입니다. 이러한 이유로 스프링 시큐리티는 Filter 레이어에서 인증/인가를 수행하게 됩니다.
이 과정에서 중요한 역할을 하는 것이 바로 FilterChain
입니다. FilterChain
은 단일 HTTP 요청을 처리하기 위해 여러 Filter들이 사슬처럼 연결된 구조로 동작합니다. 각 Filter는 정해진 순서에 따라 동기적으로 실행되며, 요청이 컨트롤러에 도달하기 전/후로 필요한 작업을 수행합니다. 예를 들어, 특정 Filter는 사용자의 인증 상태를 확인하고, 다른 Filter는 인가 정보를 검증하거나 로깅을 처리할 수 있습니다.
Filter는 Spring의
@Order
어노테이션이나Orderd
인터페이스 등 여러 방법을 이용해FilterChain
의 순서를 지정할 수 있으며, URL 패턴을 이용해 특정 요청에만 적용할 수도 있습니다.
3. FilterChain의 구조
출처: https://docs.spring.io/spring-security/reference/servlet/architecture.html
스프링 시큐리티는 서블릿 Filter를 기반으로 동작하기 때문에, FilterChain
과정을 함께 살펴보면 이해가 쉬워집니다. 위 이미지는 스프링 HTTP 통신 과정의 3번째 단계인 FilterChain
의 세부 과정을 나타낸 것입니다.
여기서 눈여겨볼 점은 DelegatingFilterProxy
라는 Filter입니다. 이는 스프링과 톰캣의 실행 타이밍이 달라 발생하는 NullPointerException
을 방지하기 위한 장치로, Lazy Loading을 통해 스프링 Bean에 안전하게 접근하기 위해 프록시 패턴을 사용합니다.
아래 Pseudo Code
는 DelegatingFilterProxy
의 호출 시점 일부를 나타낸 코드입니다.
/* DelegatingFilterProxy.java */
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
// 스프링 시큐리티의 경우, targetBeanName은 `securityFilterChain`
String targetBeanName = this.getTargetBeanName();
// targetBeanName과 일치하는 이름을 가진 Filter Bean을 찾음
Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class);
// 반환 이후 `delegate.doFilter()`를 통해 Filter 구현체를 실행
return delegate
}
이를 통해 의존성 주입(DI)과 같은 스프링의 기술을 사용할 수 있어, 스프링 시큐리티는 유연하고 확장성있는 보안 설정이 가능하며 다양한 서비스 요구 사항을 충족시킬 수 있습니다.
4. 스프링 시큐리티의 시작점, FilterChainProxy
출처: https://docs.spring.io/spring-security/reference/servlet/architecture.html
DelegatingFilterProxy
의 구현체를 보면 SecurityFilterChain
이 아닌 FilterChainProxy
로 되어 있습니다. FilterChainProxy
는 스프링 시큐리티의 시작점이자 핵심 요소로, 여러 SecurityFilterChain
을 관리하고, 방화벽 처리, SecurityContext
메모리 관리 등 스프링 시큐리티 과정에 필요한 작업을 수행합니다.
위의 작업이 완료된 후, FilterChainProxy
는 보안 작업을 위해 적절한 SecurityFilterChain
을 호출하여 개발자가 지정한 보안 Filter들을 순차적으로 실행합니다.
스프링 시큐리티의 보안 Filter 실행 순서 (스압주의)
ChannelProcessingFilter
- 프로토콜(HTTP/HTTPS)을 검사하고, 필요 시 리다이렉트 또는 거부합니다.
- HTTPS가 필수인 요청을 HTTP로 받은 경우 차단할 수 있습니다.
ConcurrentSessionFilter
- 하나의 계정으로 동시 세션 로그인을 제한합니다.
- 세션 만료 시 사용자를 로그아웃하거나 알림을 제공합니다.
WebAsyncManagerIntegrationFilter
- 비동기 요청의 보안 처리를 지원합니다.
- 비동기 요청이 끝난 후에도 SecurityContext가 올바르게 처리되도록 보장합니다.
SecurityContextPersistenceFilter
- 세션 또는 저장소에서 SecurityContext를 복원하고 저장합니다.
- 요청이 끝나면 SecurityContext를 다시 저장합니다.
HeaderWriterFilter
- HTTP 응답에 보안 헤더(CSP, X-Content-Type-Options 등)를 추가합니다.
- XSS 및 클릭재킹 공격을 방지할 수 있습니다.
CorsFilter
- CORS 정책을 검증하고 외부 도메인 요청을 제어합니다.
- 잘못된 CORS 요청을 차단합니다.
CsrfFilter
- CSRF 토큰을 검증하여 CSRF 공격을 방지합니다.
- 주로 POST, PUT, DELETE 요청에 적용됩니다.
LogoutFilter
- 로그아웃 요청을 처리하고 세션과 쿠키를 정리합니다.
- 로그아웃 후 리다이렉트할 URL을 지정할 수 있습니다.
OAuth2AuthorizationRequestRedirectFilter
- OAuth2 인증을 위한 권한 요청을 리다이렉트합니다.
- 로그인 과정에서 사용자의 동의를 요청할 때 사용됩니다.
Saml2WebSsoAuthenticationRequestFilter
- SAML2 프로토콜을 사용한 SSO 로그인 요청을 처리합니다.
- SSO 공급자와의 인증 흐름을 시작합니다.
X509AuthenticationFilter
- X.509 인증서를 사용해 사용자를 인증합니다.
- 주로 클라이언트 인증서 기반의 환경에서 사용됩니다.
AbstractPreAuthenticatedProcessingFilter
- 이미 인증된 사용자 정보가 있는 경우, 이를 사용해 인증을 처리합니다.
- 외부 인증 시스템과의 통합에 유용합니다.
CasAuthenticationFilter
- CAS (Central Authentication Service)를 통한 인증을 처리합니다.
- 주로 SSO 환경에서 사용됩니다.
OAuth2LoginAuthenticationFilter
- OAuth2 로그인 인증을 처리합니다.
- 사용자가 OAuth2 공급자(Google, Facebook 등)로부터 인증 후 돌아올 때 사용됩니다.
Saml2WebSsoAuthenticationFilter
- SAML2 프로토콜을 사용해 SSO 응답을 처리합니다.
- 인증 결과를 파싱하고 사용자 정보를 인증합니다.
UsernamePasswordAuthenticationFilter
- 폼 기반 로그인에서 사용자 아이디와 비밀번호를 인증합니다.
- 로그인 성공 또는 실패 시 이벤트를 발생시킵니다.
ConcurrentSessionFilter
- 동시 세션을 관리하며, 중복 로그인을 제한합니다.
- 사용자의 이전 세션을 만료시키거나 알립니다.
OpenIDAuthenticationFilter
- OpenID 프로토콜을 사용해 인증합니다.
- 현재는 OAuth2로 대체되는 추세입니다.
DefaultLoginPageGeneratingFilter
- 커스텀 로그인 페이지가 없는 경우 기본 로그인 페이지를 생성합니다.
DefaultLogoutPageGeneratingFilter
- 커스텀 로그아웃 페이지가 없는 경우 기본 로그아웃 페이지를 생성합니다.
DigestAuthenticationFilter
- HTTP Digest 인증을 처리합니다.
- 비밀번호를 평문 대신 해시로 전송합니다.
BearerTokenAuthenticationFilter
- Bearer 토큰(JWT, OAuth2 토큰 등)을 사용한 인증을 처리합니다.
BasicAuthenticationFilter
- HTTP Basic 인증을 처리합니다.
- 요청 헤더에 포함된 사용자 정보를 기반으로 인증합니다.
RequestCacheAwareFilter
- 인증 후 사용자가 원래 요청했던 URL로 리다이렉트합니다.
- 로그인 전 요청을 캐싱했다가 인증 후 재시도합니다.
SecurityContextHolderAwareRequestFilter
- 요청에 SecurityContext의 인증 정보를 추가합니다.
- 컨트롤러나 서비스에서 인증된 사용자 정보를 쉽게 사용할 수 있게 합니다.
JaasApiIntegrationFilter
- JAAS(Java Authentication and Authorization Service)와 통합합니다.
- JAAS 기반 인증 환경에서 사용됩니다.
RememberMeAuthenticationFilter
- 사용자가 로그인 상태를 유지하도록 Remember-Me 기능을 처리합니다.
- 브라우저 재시작 후에도 로그인 상태를 유지할 수 있습니다.
AnonymousAuthenticationFilter
- 인증되지 않은 사용자를 익명 사용자로 간주하고 기본 권한을 부여합니다.
OAuth2AuthorizationCodeGrantFilter
- OAuth2 Authorization Code Grant 흐름을 처리합니다.
- 인증 코드를 받아 액세스 토큰으로 교환합니다.
SessionManagementFilter
- 세션 고정 공격 방지와 세션 관리 정책을 적용합니다.
- 동시 세션 제한을 처리할 수도 있습니다.
ExceptionTranslationFilter
- 인증 또는 인가 실패 시 발생하는 예외를 처리합니다.
- 로그인 페이지로 리다이렉트하거나 HTTP 오류 응답을 반환합니다.
FilterSecurityInterceptor
- 요청 URL에 대한 최종 인가(Authorization)를 수행합니다.
- 사용자가 리소스에 접근할 권한이 있는지 확인합니다.
SwitchUserFilter
- 관리자가 다른 사용자로 전환해 시스템을 사용할 수 있도록 지원합니다.
- 주로 관리자 기능에서 사용됩니다.
5. 요구사항에 따라 보안 전략을 달리 구성하고 싶다면? 다중 SecurityFilterChain
출처: https://docs.spring.io/spring-security/reference/servlet/architecture.html
서비스 요구 사항 별 보안 조치를 달리 해야할 경우도 있는데, 위 사진과 같이 FilterChainProxy
를 이용해 URL 별로 로직을 분리하여 서로 다른 보안 Filter를 동작시킬 수 있습니다.
아래 Pseudo Code
는 FilterChainProxy
에서 SecurityFilterChain
을 호출하는 과정을 나타낸 코드입니다.
/* FilterChainProxy.java */
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 방화벽 설정하는 단계로 Method, URL, IP 등이 적절한지 검사
FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest)request);
// RequestMatcher를 기반으로 조건에 맞는 SecurityFilterChain를 가져옴
List<Filter> filters = this.getFilters((HttpServletRequest)firewallRequest);
// 아래 VirtualFilterChain이 모두 완료된 후 실행되는 콜백
// 기존 서블릿 FilterChain으로 돌아가기 위한 장치
FilterChain reset = (req, res) -> {
firewallRequest.reset();
chain.doFilter(req, res);
};
// 단순 Filter 리스트를 FilterChain으로 변환해주는 단계
VirtualFilterChain filterChain = this.filterChainDecorator.decorate(reset, filters);
// Security Filter를 순차적으로 수행함
filterChain.doFilter(firewallRequest, firewallResponse);
}
스프링 시큐리티의 FilterChainProxy
는 RequestMatcher
를 이용해 어떤 SecurityFilterChain
을 사용할지 결정하는 유연성을 갖추고 있습니다. 이를 통해 공통 작업은 FilterChainProxy
로 위임하며, 조건별 작업은 로직을 달리 구성할 수 있습니다.
사전 정의한 SecurityConfig
클래스에서 아래와 같이 SecurityFilterChain
을 등록하면 다중 SecurityFilterChain
을 구성할 수 있습니다.
/* SecurityConfig.kt */
@Configuration
@EnableWebSecurity
class SecurityConfig {
/* USER, ADMIN 사용자 생성 */
@Bean
fun userDetailService(): UserDetailsService {
val user: User.UserBuilder = User.builder().passwordEncoder { "{noop}$it" }
val manager = InMemoryUserDetailsManager()
manager.createUser(user.username("user").password("password").roles("USER").build())
manager.createUser(user.username("admin").password("password").roles("USER", "ADMIN").build())
return manager
}
/*
'/admin' 경로로 접근 시 해당 FilterChain을 사용하게 됨
'ROLE_ADMIN' 권한이 없으면 403 Forbidden 발생
*/
@Bean
@Order(1)
fun adminFilterChain(http: HttpSecurity): SecurityFilterChain {
http.securityMatcher("/admin")
http.authorizeHttpRequests { it.anyRequest().hasRole("ADMIN") }
http.httpBasic { }
return http.build()
}
/*
이 외의 URL로 접근하면 해당 FilterChain을 사용하게 됨
@Order 어노테이션이 없으면 후순위로 동작함
*/
@Bean
fun defaultFilterChain(http: HttpSecurity): SecurityFilterChain {
http.authorizeHttpRequests { it.anyRequest().authenticated() }
http.formLogin { }
return http.build()
}
}
FilterChainProxy
에서 securityMatcher
를 기준으로 SecurityFilterChain
을 결정하게 됩니다. 이를 통해 서비스 요구 사항에 따라 보안 Filter 구성을 달리 할 수 있습니다.
위 코드에서 구성된 SecurityFilterChain
은 아래 Pseudo Code
와 같이 FilterChainProxy
에서 선택됩니다.
/* FilterChainProxy.java */
private List<Filter> getFilters(HttpServletRequest request) {
Iterator iter = this.filterChains.iterator();
SecurityFilterChain chain;
do {
if (!iter.hasNext()) {
return null;
}
chain = (SecurityFilterChain) iter.next();
/*
반복문을 돌며 RequestMatcher를 기반으로 조건에 맞는 FilterChain을 선택합니다.
URL, Request Header 등 다양한 조건을 설정할 수 있습니다.
*/
} while (!chain.matches(request));
return chain.getFilters();
}
6. 동작 구조 요약
- DelegatingFilterProxy가 호출되면 WebApplicationContext를 참조해 Filter 구현체인 FilterChainProxy를 실행합니다.
- NullPointerException을 방지하기 위해 WebApplicationContext가 준비될 때까지 기다립니다.
- 내부적으로 DelegatingFilterProxy -> WebMvcSecurityConfiguration -> CompositeFilter -> FilterChainProxy 순의 호출 과정이 존재합니다.
- FilterChainProxy는 요청 조건에 따라 적절한 SecurityFilterChain을 선택하고 호출합니다.
- RequestMatcher를 이용해 미리 지정한 조건을 따릅니다.
- 선택된 SecurityFilterChain 내의 보안 Filter들을 순차적으로 실행합니다.
- 모든 Filter를 통과하면 DispatcherServlet으로 요청을 전달하며, 스프링 시큐리티 필터링 과정이 종료됩니다.
- 이후에도 SecurityContextHolder를 통해 인증 및 인가 정보를 확인할 수 있습니다.
📚 참고하면 좋은 자료
- Spring Security 공식 문서
- Servlet Security, The Big Picture - 토리맘의 한글라이즈 프로젝트
- 필터(Filter)가 스프링 빈 등록과 주입이 가능한 이유(DelegatingFilterProxy의 등장)
Spring Security