
https://dev-dx2d2y-log.tistory.com/217
[백엔드] Spring Security 첫 걸음 - DelegatingFilterProxy, FilterChainProxy로 Web Context Filter에 Spring Bean Filter 등
DelegatingFilterProxy그 전에 그냥 HTTP 요청이 어떻게 처리되냐면..클라이언트로부터 요청이 들어오면 HttpServletRequest, HttpServletResponse 객체가 생성되며, 이 객체는 필터들을 통과하며 조건에 부합하는
dev-dx2d2y-log.tistory.com
SpringSecurity 단계에서 저번에는 DelegatingFilterProxy와 FilterChainProxy에 대해서 알아보았다.
스프링에서 관리하는 서블렛 필터들은 FilterChainProxy에 담겨서 톰캣서버의 필터로 등록되고, DelegatingFilterProxy가 FilterChainProxy를 등록해주는 역할을 담당한다.
Spring Security에서 FilterChainProxy는 SecurityFilterChain이 담당한다. SecurityConfig 파일에서 정의한 보안속성들이 하나의 필터가 되어서 SecurityFilterChain을 만들고 톰캣서버에 필터로 등록되는거.
따라서 이번에는 Spring Security에 속한 하위 필터들에 대해 알아보고자한다.
SecurityContextHolderFilter

원래 이 자리에는 SecurityContextPersistenceFilter가 들어가야하나, 스프링 5.7 이후로 Deprecated 되었고 SecurityContextHolder가 그 자리를 차지하게되었다.
Spring Security가 담당하는 인증정보는 SecurityContext라는 곳에 저장되는데, SecurityContextHolderFilter는 SecurityContextRepository에서 이 인증정보를 가져와 SecurityContextHolder에게 전해준다.

SecurityContextHolder와 SecurityContext의 내부에 있는 Authentication 객체가 실제 인증과정에서 사용된다. 그래서 스프링은 Authentication 객체에 인증된 사용자의 세부정보를 저장한다. Authentication 객체는 크게 인증과정에서 다음 인증과정으로 인증정보를 넘기거나, 인증이 완료된 사용자의 정보를 담는 역할을 한다. 내부에 isAuthenticated() 메서드가 있는데, 호출할 시 전자는 false를 반환한다.
세션에 인증이 없더라도 SecurityContextHolder는 인증을 담아둔다. 만약 로그인 요청이 아니라면 그대로 필터를 타고 가고, 로그인 요청이라면 아래 AuthenticationFilter가 동작하게된다.
인증과정이나 로그인 도중에 사용하는 SecurityContext는 ThreadLocal로 잠시저장한다. 어느 필터에서든 SecurityContext에 값이 변경되면 ThreadLocal에 있는 SecurityContext에 변경사항을 저장하고, 모든 인증과정이 끝나면 SecurityContextRepository가 HttpSession에 저장한다.
만약 요청이 들어오면 HttpSession에 있는 SecurityContext를 가져와서 ThreadLocal에 세팅하고, 인증과정에서는 ThreadLocal에 있는 SecurityContext를 사용한다.
LogoutFilter
로그아웃을 담당한다. 유저의 정보를 알았다면 로그아웃 상황 시에는 이후의 인증과정이 필요없으므로 유저 정보를 가져온 후 제일먼저 거치는 필터인듯?
LogoutFilter에서는 세션무효화, AccessToken 삭제, SecurityContext에서 해당 토큰을 삭제한다. 그러니까 로그아웃과 관련해서 토큰삭제, 세션삭제 등을 진행한다. 다만 JWT토큰 인증방식을 사용할 때에는 세션 방식을 사용하지 않기 때문에 굳이 필요하지는 않은 편. 세션로그인을 커스텀해서 사용할 때 필요하다.
AuthenticationFilter
AuthenticationFilter는 로그인 요청이 들어왔을 때 사용자가 보낸 정보를 토대로 인증을 수행하는 역할을한다. 만약 로그인 요청이 아닐 경우에는 동작하지 않는다.
인증을 수행하기 위해서 필터체인은 Authentication 이라는 객체를 만들어 여기에 유저가 보낸 정보를 저장한다. username, password 같은 정보.
먼저 Authentication 객체에 대해 알아보자면, Authentication 객체의 3가지 하위요소로는
- Principal
사용자 식별에 사용한다. 주로 UserDetails의 인스턴스가 이 역할을 담당한다.
- Credentials
비밀번호를 담당한다. 정보가 유출되지 않음을 보장하기 위해서 인증 후에는 제거된다.
- Authorities
사용자에게 부여된 권한을 확인한다. GrantedAuthority의 인스턴스를 사용한다.
GrantedAuthority는 "ROLE_USER", "ROLE_ADMIN"과 같은 형식으로 유저에게 부여된 권한을 저장하며, 나중에 스프링에게 권한확인을 위임할 수 있는데, 권한확인 시 여기의 권한을 확인하게된다.
주의할 점은 GrantedAuthority는 애플리케이션 전반에 걸쳐서 적용되는 권한을 나타낸다. 이를테면 사용자, 관리자, VIP회원 등.. 따라서 객체 단위로 접근권한은 GrantedAuthority를 사용해서는 안된다. 이를테면, "게시글 130625번에 접근할 수 있는사람"과 같이 객체 하나하나에 접근권한을 세워서는 안되고, 이는 ACL (Access Control List)를 사용해서 서비스레이어에서 처리해야한다.
이는 가능한 권한이 매우 많으면 그만큼 스프링이 인증 시 권한확인에 시간이 오래 걸리고, 이러한 권한이 매우 많아지다보면 메모리 누수도 발생할 수 있기 때문이다.
따라서 AuthenticationFilter에서는 Authentication 객체를 만든다.
폼로그인 방식이라면 UsernamePasswordAuthenticationFilter가 이를 수행할 것이고, OAuth2 로그인 방식이라면 OAuth2LoginAuthenticationFilter, httpBasic 로그인 방식이면 BasicAuthenticationFilter가 이를 수행한다. 그리고 이는 SecurityConfig 파일에서 지정한다. 뭐 http.formLogin()을 쓰면 폼로그인방식을 쓰게되는 것이고, .oauth2()를 쓰면 OAuth2 로그인 방식을 쓰는 것이고.. 둘 다 쓸 수도 있다.
AuthenticationManager
무튼 AuthenticaitionFilter의 일은 Authentication 객체를 생성하는 일이고, 실제로 Authentication 객체에 있는 유저의 정보가 인증되었는지 여부는 AuthenticationManager가 담당한다.

ProviderManager
AuthenticationManager를 사용해야할 경우, 가장 널리 사용되는 구현체는 ProviderManager이다.

ProviderManager는 다시 AuthenticationProvider에게 이 인증과정을 위임한다. 만약 아이디/비밀번호 방식 인증이라면 DaoAuthenticationProvider가 사용될 것이고, JWT토큰으로 인증을 처리한다면 JwtAuthenticationProvider가 사용된다. 각 AuthenticationProvider의 목적은 인증을 수행하고, 인증에 성공하면 Authentication 객체를 생성해 반환하고, 실패하면 예외를 던진다.
ProviderManager는 성공적으로 인증이 진행되었다면 Authentication 객체 내부에 있는 Credentials 부분을 지운다. 인증이 끝났으므로 더이상 불필요하지만, 탈취되면 위험한 정보가 HttpSession에서 필요이상 존재하는 것을 방지하기 위해서다.
ProviderManger는 여러 AuthenticationProvider를 List 형태로 받아 여러 AuthenticationProvider들에게 인증을 위임할 수 있는데, 이는 downstream 방식으로 진행된다. AuthenticationProvider는 인증성공, 인증실패 외에 인증불가 상태가 있는데, 만약 AuthenticationProvider0이 인증불가를 반환하면 그 뒤에오는 AuthenticationProvider1이 인증을 수행하게된다.
만약 주어진 모든 AuthenticationProvider가 인증불가상태를 띄우면, ProviderNotFoundException 에러를 던진다. 이 에러는 AuthenticationException의 하위에러다.
ProviderManager는 자신이 가진 모든 AuthenticationProvider가 인증을 수행할 수 없다면 ProviderNotFoundException을 던지는데, 예외적으로 다른 AuthenticationManager에게 이 인증과정을 처리하게할 수 있다. ProviderManager를 설정할 때 인증을 넘길 AuthenticationManager를 선언하면된다. 그리고 여기서 인증이 넘어가는 AuthenticationManager는 대개 ProviderManager의 하위인스턴스다. 왜냐하면 스프링 인증과정이 대개 ProviderManager를 위주로 돌아가기 때문이라고한다.
만약 동일한 API에서 세션인증과 JWT토큰 인증을 같이 사용한다면, Jwt용 ProviderManager가 인증불가를 던지면 에러가 발생하는게 아니라 세션용 ProviderManager에게 이 인증과정을 처리하게한다.
물론 ProviderManager는 AuthenticationProvider를 List 형태로 여러 개 사용할 수 있어서 이러지는 않고, 실제로는 다중 SecurityChain 환경에서 ProviderManager를 재사용하기 위해 사용한다. 모든 체인마다 ProviderManager를 중복으로 등록시키는게 아니라, 각 체인에 연결된 ProviderManager가 인증실패를 기본으로 띄우고, 공용 ProviderManager에게 이 인증을 넘기는 식으로 동작한다고한다.
인증 이후의 과정 (AuthenticationFilter)

암튼 그래서 이정도의 플로우를 가지고있다.
1. 사용자가 로그인 요청을 보내면, AbstractAuthenticationProcessingFilter가 사용자가 보낸 요청을 토대로 Authentication 객체를 생성해 AuthenticationManager로 정보를 넘긴다. Authentication의 실제 타입은 AbstractAuthenticationProcessingFilter의 실제 타입에 따라 다르다.
2. Authentication 객체는 AuthenticationManager에 의해서 인증절차를 거친다. 절차는 위에서 설명한 바와 같다.
3. 만약 인증에 실패한다면 3번과도 같은 과정을 거친다.
3-1. 우선 Authentication 객체를 들고 있는 SecurityContextHolder를 비운다. 즉, 인증에 실패한 사용자의 정보가 담긴 Authentication 객체를 지운다.
3-2. RememberMeService.loginFail 메서드가 실행된다. 사용자는 세션로그인 기반인 스프링의 경우에는 서로 다른 세션의 경우를 대비하여 RememberMe라는 토큰을 발급받는데, 이 토큰도 삭제한다. 정의되지 않으면 동작하지 않는다.
3-3. AuthenticationFailureHandler가 호출된다. 개발자는 이 인터페이스를 구현하여 구체적으로 인증에 실패할 경우 기능을 정의할 수 있다.
4. 만약 인증에 성공하면 4번과도 같은 과정을 거친다.
4-1. SessionAuthenticationStrategy가 동작한다. 세션인증 전략을 정의한다. 한 사용자가 동시에 여러 세션에서 접근할 수 없게 설정하려는 등, 세션인증에 관한 여러 설정들을 고를 수 있다.
4-2. SecurityContextHolder에 Authentication 객체를 저장한다. 이미 있다면, 덮어쓰기한다. 만약 이 Authentication 객체를 세션에 저장해 나중에 요청에 포함시키고 싶다면, SecurityContextRepository 클래스의 saveContext() 메서드를 통해 세션에 저장해야한다.
Servlet Authentication Architecture :: Spring Security
ProviderManager is the most commonly used implementation of AuthenticationManager. ProviderManager delegates to a List of AuthenticationProvider instances. Each AuthenticationProvider has an opportunity to indicate that authentication should be successful,
docs.spring.io
영어해석실패이슈로 해석과 지피티 내용을 조금 섞어서 작성했다. 진짜 본연의 문장은 위 링크를 참고
4-3. 앞서 RememberMe 토큰을 사용한다고했는데, RememberMeServices.loginSuccess() 메서드를 통해 RememberMe 토큰을 발급받는다.
4-4. ApplicationEventPublisher가 호출되어 InteractiveAuthenticationSuccessEvent를 발행한다. 왜 발행하는지 블로그 글이 잘 없어서 지피티에게 물어보니 로그인 후 로그저장, 로그인 알림 발송, 로그 작성과 같이 로그인에 대한 부가적인 기능을 처리해야한다. 이 기능을 필터체인에 넣으면 비즈니스 로직과 인증로직이 서로 엉키게되는데, 따라서 별도의InteractiveAuthenticationSuccessEvent 리스너 클래스를 만들어 여기서 비즈니스 로직을 처리한다. 그래서 필요한 이유.
4-5. AuthenticationSuccessHandler 가 호출된다. 실무상에서 AuthenticationSuccessHandler는 로그인에 대한 HTTP 응답 생성, JWT토큰 발급 등에만 관여해야한다고한다. 나머지 모든 기능들 (로그인 메일 발송 등..)은 위에서 말한 별도의 리스너 클래스에서 다뤄야한다고..
정리
여기까지가 "로그인과정"에서의 필터체인들이다.

로그인 요청이 들어오면, SecurityContextHolderFilter가 SecurityContextRepository를 호출해 HttpSession에 있는 SecurityContext를 가져와 ThreadLocal에 저장한다. 이 SecurityContext에는 username이나 password, 유저의 권한 등 인증과정에서 사용할 사용자 정보가 들어있는 Authentication 객체가 들어있고, 처음 로그인 요청을 보내면 SecurityContext는 비어있다. (Authentication이 null)
SecurityContextHolderFilter 이후에는 AuthenticationFilter가 호출된다. AuthenticationFilter의 구현체로는 대표적으로 UsernamePasswordAuthenticationFilter (아이디 / 비밀번호 형식의 폼로그인에서 사용), OAuth2LoginAuthenticationFilter (OAuth2 로그인에서 사용) 등이 있으며, 각 AuthenticationFilter에서는 유저가 보낸 정보를 토대로 Authentication 객체를 생성한다.
AuthenticationFilter에서는 Authentication객체를 생성한 후 AuthenticationManager를 호출해 인증을 처리한다. 대표적인 AuthenticationManager의 구현체로는 ProviderManager가 있다.
AuthenticationManager에서는 AuthenticainoProvider를 호출해 인증을 수행한다. AuthenticationProvider는 인증상태를 인증성공, 인증실패, 인증불가로 표현할 수 있다. AuthenticationProvider는 여러 AuthenticationProvider들을 List 형태로 받아서 한 AuthenticationProvider가 인증실패를 띄우면 다음 AuthenticationProvider가 인증을 수행한다. 어느 AuthenticaionProvider도 인증을 수행하지 못하면 ProviderNotFoundException을 던지거나 개발자가 예외적으로 다른 AuthenticaionManager에게 인증을 넘길 수 있도록 설정할 수 있다.
1) AuthenticationManager에서 인증이 실패한 경우
AuthenticationFilter가 SecurityContextHolder에 있는 Authentication를 비운다. 세션로그인 방식의 경우에는 하나의 세션(SecurityContext)에 하나의 인증정보만 저장되고, 로그아웃 시에는 세션을 비우게되므로 결국 인증상황에서는 세션에 로그인 정보가 없다. 따라서 로그인이 실패한 환경에서는 세션에 인증정보가 없다는 것이 자명한 상황이다. 그런데 왜 SecurityContext를 지우는지 궁금해서 지피티에게 물어보니..
만약의 비정상적 로그인상황에 대비하는 목적이기도하고, 앞서 설명한대로 SecurityContext 등 스프링 시스템은 ThreadLocal을 기반으로 사용한다. 같은 스레드를 여러 번 재사용해야기 때문에 인증실패 시 안정적인 동작을 위해 SecurityContext를 지운다. 다음 요청을 보낼 때 SecurityContext가 꼬이지 않도록 지운다.
그리고 스프링에서는 기본적으로 세션로그인을 지원하는데, 다른세션에서도 로그인을 유지하기 위해 RememberMe 토큰을 발급하는데, 역시 RememberMe 토큰을 폐기한다.
마지막으로 AuthenticationFailureHandler를 호출해 추가적으로 실패 시 로직을 처리한다.
2) AuthenticationManager에서 인증이 성공한 경우
AuthenticationFilter가 SessionAuthenticationStrategy를 호출해서 세션로그인 관련 보안설정들을 설정한다. 그리고 Authentication 객체를 SecurityContext에 저장한다. 기존값이 있으면 덮어쓰기. 그리고나서 RememberMe 토큰을 발급한다.
그리고 ApplicationEventPublisher를 호출해 InteractiveAuthenticationSuccessEvent를 발행한다. 이를 발행하는 이유는 InteractiveAuthenticationSuccessEvent의 리스너 클래스가 발행을 감지하고 로그인 성공 이후의 로그처리, 로그인 메일발송 등 비즈니스 로직을 처리한다.
그리고나서 AuthenticationSuccessHandler를 호출해서 추가적으로 성공 시 로직을 처리한다. 일반적으로 JWT토큰 발급이나 HTTP Response 생성 같은 과정은 SuccessHandler에서 관리한다고한다.
'CS > 백엔드' 카테고리의 다른 글
| [백엔드] 스프링 컴포넌트 스캔과 Bean 등록하기 및 DI (0) | 2026.03.16 |
|---|---|
| [백엔드] 인증요청 시의 SecurityFilterChain의 동작과정 (0) | 2026.02.18 |
| [백엔드] Spring Security 첫 걸음 - DelegatingFilterProxy, FilterChainProxy로 Web Context Filter에 Spring Bean Filter 등록하기 (0) | 2026.02.14 |
| [백엔드] OAuth는 어떻게 진행되는가? (0) | 2026.02.06 |
| [BckEnd] 스프링없이 순수JDK로 간단한 개발해보기 (0) | 2026.01.04 |
