세세한 걸 알아보기에 앞서, 전체적인 동작을 확인해 보자.
우선, Spring Security는 Servlet의 Filter 기반으로 작동한다.

문제는 여기에 있다. Filter는 Spring Context 밖에 있는 녀석이다.
그 말은, Bean을 인식하지 못한다는 거다.
그렇기에 Servlet의 FilterChain에 DelegatingFilterProxy라는 이름만 들어도 껍데기 냄새가 폴폴나는 필터를 하나 집어넣는 트릭을 쓴다.
// Servlet의 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
}
// DelegatingFilterProxy의 SecurityFilterChain
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// Lazily get Filter that was registered as a Spring Bean
// For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0
Filter delegate = getFilterBean(someBeanName);
// delegate work to the Spring Bean
delegate.doFilter(request, response);
}
DelegatingFilterProxy는 Bean으로 등록된 Filter들을 찾아내 받는 것이 일이다.
이 녀석은 Spring Context에 속해있기 때문에 당연히 그게 가능하다.
이런 구조를 취함으로써 얻는 이득이 하나 더 있다.
Bean으로 등록된 Filter들을 찾는 시점을 늦출 수 있다는 것이다.
이게 왜 중요하냐면, 컨테이너는 기본적으로 컨테이너로써 동작하기 전에 Filter 구성을 끝마친다. 그런데 Spring은 ContextLoaderListener를 사용해 빈을 등록한다. 문제는 이게 한참 뒤에 실행되기 때문에 Bean으로 등록된 Filter들은 정작 Servlet의 Filter들이 구성될 때에 로드가 되지 않은 상태이기 때문이다.
간단히 비유하자면,
'여기 치킨 배달 되죠?' -> '네, 됩니다. 바로 나갑니다.' 해놓고는 뒤에서 생닭 사러 갔다는 거다.
DelegatingFilterProxy에 의해 찾아내진 Bean들은 이 FilterChainProxy에 등록된다.
그림으로 표현하자면 이렇다.

이 녀석도 이름따라 껍데기인데, 실질적인 필터링은 등록된 SecurityFilterChain 내부의 필터들에게 위임하는 역할을 한다.
왜 프록시에 프록시를 넣는 이따위 구조를 쓰냐?
장점이 몇 개 있다.
FilterChainProxy에 등록해버릴 수 있다. SecurityContext를 지운다던가, HttpFirewall을 적용하여 특정 유형의 공격으로부터 애플리케이션을 보호한다던가.SecurityFilterChain을 호출 시기를 결정하는 데에 있어 더 많은 유연성을 제공한다.RequestMatcher 인터페이스를 이용해 HttpServletRequest의 값을 기반으로 호출을 결정할 수 있다.SecurityFilterChain이 있을 때, 어떤 것을 사용해야 할 지도 정할 수 있다.아무튼, Bean으로 등록된 Filter들은 SecurityFilterChain이라는 내부의 필터 체인에서 돌아가게 된다.
만약 Filter를 제대로 통과하지 못한다면 인증이 요구된다.
Spring Security를 적용하고나서 어플리케이션을 실행했을 때, 로그인 화면으로 가는 것이 이 때문.
성공적으로 로그인을 해 인증을 하면, 해당 정보는

이렇게 SecurityContextHolder 내부의 SecurityContext 안의 Authentication에 저장된다.
Principal은 유저 ID, Credentials은 비밀번호, Authorities는 권한으로 보면 된다.
필터의 종류가 정말 많다.
일단 공식 사이트에 있는 것만 32개인데, 이런 사이트 특성 상 업데이트가 잘 안된다는 걸 생각해보면 더 많을 수도 있지 않을까?
그런고로, 모든 필터가 다 알고 싶다면 밑에 References에서 첫 번째 사이트로 간 다음 9.5 항목을 살펴보자.
여기서는 주로 사용되는 필터 위주로 알아보도록 하겠다.
DefaultLoginPageGeneratingFilter : 기본 로그인 페이지를 제공한다.
DefaultLogoutPageGeneratingFilter : 기본 로그아웃 페이지를 제공한다.
CorsFilter : CORS (Cross-origin resource sharing)에 대한 검사를 한다.
CsrfFilter : CSRF (Cross-site request forgery)에 대한 검사를 한다.
LogoutFilter : logout request인지에 대한 검사를 한다.
SecurityContextPersistenceFilter :
SecurityContext를 여러 요청에 걸쳐 공유하게 해 준다.SecurityContextRepository 인터페이스를 이용하는데 디폴트 구현체는 HttpSessionSecurityContextRepository이다.httpSession에 SecurityContext를 넣어두고 있다가, 다음 요청이 오면 SecurityContextHolder에서 SecurityContext를 꺼낸다.SecurityContextHolder를 비운다.SecurityContext를 SecurityContextRepository에 저장한다.SecurityContextHolder는 쓰레드별로 SecurityContext를 관리하기 때문. Servlet은 요청마다 쓰레드를 새로 생성하기 때문에, 새로운 요청에선 기존 인증 정보에 접근을 못한다.RememberMeAuthenticationFilter :
RemberMeServices 인터페이스를 활용한다.AbstractRemeberMeServices가 있으며, 이를 상속한 TokenBaseRememberMeServicese, PersisenceTokenBasedRememberMeServicese가 있다.TokenBaseRememberMeServicese는 토큰을 브라우저 쿠키에 저장하고 서버에는 저장하지 않는다. ID와 만료시간 변환된 암호값이 그대로 토큰에 들어가기 때문에, 만약 토큰이 탈취되면 만료시간이 끝나거나 비밀번호가 변경되지 않는 한 ID가 공공재가 되어버린다.PersisenceTokenBasedRememberMeServicese이다. ID와 만료시간 대신 토큰의 시리즈 값을 넘겨준다. 대신 서버에서는 PersistentTokenRepository 인터페이스의 구현체인 JdbcTokenRepositoryImpl를 통해 DB로 토큰을 관리한다.CookieTheftException이 발생되며, 해당 유저에게 발급되었던 토큰은 DB에서 모두 제거되어 버린다. 그렇기에 탈취 된 토큰은 더이상 유효하지 않게 된다.AnonymousAuthenticationFilter : Security Filter 처리를 위해서는 어쨌거나 Authentication이 null이어선 안된다. 그렇기 때문에 인증 정보가 아직 존재하지 않는다면, 익명 사용자용으로 아무것도 인증하지 못하는 빈 껍데기 같은 걸 하나 내준다.
UsernamePasswordAuthenticationFilter : username과 password를 이용한 Form 로그인 방식을 지원한다.
OAuth2AuthorizationRequestRedirectFilter :
OAuth2AuthorizationRequestResolver 인터페이스의 구현체인 DefaultOAuth2AuthorizationRequestResolver를 이용해 어느 사이트의 인증이 필요한지를 알아낸다.OAuth2AuthorizationRequest 객체를 만들어 해당 사이트로 redirect 요청을 보낸다.OAuth2LoginAuthenticationFilter :
ClientRegistration을 통해 인증의 결과를 받는다. 이후, OAuth2AuthorizationResponse 객체로 authorization code를 생성해서 OAuth2LoginAuthenticationToken을 얻는다.OAuth2LoginAuthenticationProvider 라고 하는 ProviderManager의 일종이 일을 한다.DefaultAuthorizationCodeTokenResponseClient 구현체를 이용해 OAuth2 server에서 토큰을 요청한다.BasicAuthenticationFilter : SPA이나 모바일 환경에서 요청 헤더에 토큰을 담아서 로그인하는 Ajax 로그인을 지원한다.
ConcurrentSessionFilter : 매 요청마다 현재 사용자의 세션 만료 여부를 체크한다. 만료 여부는 SessionManagementFilter에서 설정한 값을 따른다. 세션이 만료되었을 경우, 로그아웃을 시키고 세션 만료에 관한 처리를 한다.
SessionManagementFilter :
http.sessionManagement.maximumSessions(1)과 같은 식으로 최대 세션 개수를 정한다. 즉, 중복 로그인을 막는다는 것. 2나 3 등으로 해도 된다.IF_REQUIRED, ALWAYS, NEVER, STATELESS가 있다.ALWAYS는 언제나 Spring Security가 세션을 생성, NEVER는 하지 않지만 이미 존재한다면 사용한다. IF_REQUIRED는 기본값이며 필요하다면 생성한다. STATELESS는 생성하지 않고 존재해도 사용하지 않는다. STATELESS는 주로 JWT 인증 방식일 때 사용된다.FilterSecurityInterceptor : 사용자의 request에 대해 인증과 권한을 확인한다. 통과 못 할 경우에 Exception이 던져지고, ExceptionTranslationFilter에 의해 처리된다.
ExceptionTranslationFilter : FilterSecurityInterceptor가 AccessDecisionManager를 통해 AuthenticationException 혹은 AccessDeniedException을 발생시킨 경우, Exception을 받아 각각 AuthenticationEntryPoint, AccessDeniedHandler로 보낸다.
EnableWebSecurity(debug = true)로 하고 security filter chain을 통과하게 만들면 볼 수 있다.
아래의 순서를 따른다.
HttpSecurity.httpBasic().disable() 따위의 설정으로 인해 적용 해제 된 필터도 있다.

각 과정에 대해 살펴보자.
ExceptionTranslationFilter가 필터 체인을 호출한다.AuthenticationException 예외가 던져졌다면, 인증 절차를 시작한다.SecurityContextHolder를 비운다.HttpServletRequest가 RequestCache에 저장된다. 유저가 성공적으로 인증을 했다면, RequestCache는 원본 요청을 다시 불러오는데 쓰일 수 있다.AuthenticationEntryPoint는 사용자를 로그인 페이지로 다시 보내거나 하여 credential을 요구한다. AccessDeniedException이 발생한 경우, AccessDeniedHandler에게 제어가 넘어가며 접근이 거부된다.아래는 ExceptionTranslationFilter의 수도 코드이다.
주석이 있는 부분이 위 그림의 1, 2, 3번과 일치한다.
try {
filterChain.doFilter(request, response); // 1
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication(); // 2
} else {
accessDenied(); // 3
}
}