Spring Security, 제대로 이해하기 - FilterChain

2022. 4. 14. 22:56Spring

반응형

Spring Security의 인증, 인가 과정을 FilterChain을 살펴보며 이해하는 것이 본 포스팅의 목표입니다.

 

해당 포스팅은 1부  Spring Security, 어렵지 않게 설정하기의 이은 포스팅이지만, 읽는데 순서는 상관없습니다.

이해하는데 서로 도움이 되는 포스팅입니다.

 

-----------------    INDEX     -----------------

 

[ 1부: 설정 ]

WebSecurityConfigurerAdapter

HttpSecurity

WebSecurity

 

[ 2부 - 인증, 인가 동작 원리: FilterChain]

📚  Big Picture

초기화

사용자 요청 후 ?

🔗  FilterChain

SecurityContextPersistenceFilter

LogoutFilter

UsernamePasswordAuthenticationFilter

ConcurrentSessionFilter

RememberMeAuthenticationFilter

AnonymousAuthenticationFilter

SessionManagementFilter

ExceptionTranslationFilter

FilterSecurityInterceptor

----------------------------------------------

 

 

 

📚  Big Picture

 

 

위 그림은 사용자가 인증을 할 때의 큰 그림을 나타낸 것입니다.

이 그림을 이해할 때 사용자의 인증 과정 뿐만 아니라, 인증된 사용자의 인가과정이 어떻게 진행되는지를 파악하는 것이 핵심입니다.

 

 

초기화

 

Spring Security에서는 인증, 인가에 대한 처리를 여러개의 필터를 연쇄적으로 실행하여 수행합니다.

이 때, 설정에 따라서 필요한 필터가 있고 필요 없는 필터가 있을 텐데요.

그 설정은 WebSecurityConfigurerAdapter를 구현한 설정 파일의 내용을 기반으로 해당되는 필터들을 생성합니다.

WebSecurityConfigurerAdapter을 통한 설정은 이전 포스팅에서 다룹니다.

 

이 때, 실제 필터를 생성하는 클래스가 바로 HttpSecurity입니다.

HttpSecurity의 코드를 보면 DefaultSecurityFilterChain 객체를 생성하거나 addFilter 메소드와 같이 필터를 다루는 것을 확인할 수 있습니다.

 

이렇게 설정 파일 별로 필터 목록을 갖게 된 후, 이 필터들은 WebSecurity 클래스에게 전달이 됩니다.

WebSecurity는 각각 설정 클래스로 부터 필터 목록들을 전달받고, 다시 FilterChainProxy를 생성자의 인자로 전달합니다.

 

결국 FilterChainProxy는 각각의 설정 클래스 별(SecurityConfig1, SecurityConfig2)로 필터 목록들을 갖고 있는 형태가 됩니다.

 

 

사용자 요청 후 ?

 

 

사용자가 처음 요청을 하면 가장 먼저 DelegatingFilterProxy가 가장 먼저 그 요청을 받고,

FilterChainProxy에게 요청을 위임합니다.

 

DelegatingFilterProxy는 서블릿 필터이며 위임을 할 때에는 "springSecurityFilterChain"이라는 이름을 가진 Bean을 찾게 되는데, 그 Bean이 바로 FilterChainProxy입니다.

위에서 확인했듯이, FilterChainProxy는 초기화될 때 이미 빈으로 등록되며 필터 목록을 가지고 있습니다.

 

이제 위임받은 요청을 가지고 있는 각각의 Filter들에게 순서대로 요청을 맡깁니다.

이 때 각각의 필터들이 체인으로 연결되어 수행 → 넘김 → 수행 → 넘김으로 진행되는 형태입니다.

이 때, 수행되는 메소드가 doFilter입니다.

 

 

 

🔗  FilterChain

 

이제부터 본 포스팅의 핵심인 FilterChain을 알아보겠습니다.

 

 

각각의 필터가 무엇을 담당하는지, 어떤 동작을 수행하는지 확인해볼게요.

 

 

SecurityContextPersistenceFilter

SecurityContextRepository에서 SecurityContext를 가져오거나 생성

 

SecurityContextRepository는 내부적으로 HttpSessionSecurityContextRepository 클래스를 가집니다.

private SecurityContextRepository repo; 의 형식으로 참조하고 있습니다.

이 HttpSessionSecurityContextRepository 클래스가 SecurityContext 객체를 생성하고, 세션에 저장합니다.

또, 세션에 저장된 SecurityContext를 조회하고 참조하는 클래스입니다.

 

 

SecurityContextRepository에서는 loadContext 메소드로

인증을 시도한 사용자가 이전에 세션에 저장한 이력이 있는지 확인합니다.

 

✔️ 처음 인증하거나 혹은 익명 사용자일 경우

세션에 저장된 것이 없을 테니 SecurityContext을 생성합니다.

SecurityContextHolder안에 저장을 하고 다음 필터를 실행합니다.

 

✔️ 이력이 있을 경우

SecurityContext를 꺼내와서 SecurityContextHolder에 저장합니다.

따라서 SecurityContext를 따로 생성하지 않습니다.

 

모든 작업이 마쳐서 최종적으로 클라이언트에게 인증하기 직전에는 항상 Clear SecurityContext를 실행합니다.

 

 

LogoutFilter

로그아웃 요청을 처리

 

로그아웃 요청 시에만 실행합니다.

 

 

UsernamePasswordAuthenticationFilter

ID와 Password를 사용하는 실제 Form 기반 유저 인증을 처리

 

인증 객체를 만들어서 Authentication 객체를 만들어 아이디 패스워드를 저장하고,

AuthenticationManager에게 인증처리를 맡깁니다.

 

Authentication?

Authentication객체는 인증 시 id 와 password 를 담고 인증 검증을 위해 전달되어 사용됩니다.

인증 후 최종 인증 결과 (user 객체, 권한정보) 를 담고 SecurityContext 에 저장되어 전역적으로 참조가 가능합니다.

사용자의 인증 정보를 저장하는 토큰 개념입니다.

 

 

AuthenticationManager가 실질적인 인증을 검증 단계를 총괄하는 클래스인 AuthenticationProvider에게 인증 처리를 위임합니다. 그럼 AuthenticationProvider가 UserDetailsService와 같은 서비스를 사용해서 인증을 검증합니다.

 

최종적으로 인증을 성공한 경우, 인증에 성공한 결과를 담은 인증객체(Authentication)를 생성한 다음에 SecurityContext에 저장합니다.

이 때, SecurityContextHolder 안에 있는 SecurityContext는 SecurityContextPersistenceFilter에서 생성하고 저장한 객체를 참조하겠죠.

 

인증 후 후속처리를 할텐데, 이때 SessionManagementFilter가 가진 Register SessionInfo, SessionFixation, Concurrentsession을 인증을 시도하는 당시 동시에 진행합니다.

설명은 아래 SessionManagementFilter를 소개할 때 자세히 설명하도록 하겠습니다.

 

 

ConcurrentSessionFilter

동시 세션과 관련된 필터

 

현재 사용자 계정으로 인증을 받은 사용자가 두 명 이상일 때 실행되는 필터입니다.

 

이미 로그인을 했는데, 같은 계정으로 다른 사람이 로그인을 시도하거나 다른 곳에서 다시 로그인을 시도할 때 세션이 두 개 이상으로 늘어날 수 있습니다.

ConcurrentSessionFilter는 위의 같은 상황과 같이 동시 세션과 관련된 필터입니다.

 

해당 필터는 매 요청마다 현재 사용자가 세션이 만료되었는지 확인합니다.

이 때 session.expireNow의 값으로 만료 설정을 구분합니다.

이 값은 아래 SessionManagementFilter에서 설정이 됩니다. 이 부분도 SessionManagementFilter에서 자세히 다루겠습니다.

 

그래서 만약 사용자가 리소스에 접근 중이다가 session.expireNow 값이 true가 되면

해당 사용자를 로그아웃하고 요청 실패를 던집니다.

 

 

RememberMeAuthenticationFilter

세션이 사라지거나 만료 되더라도, 쿠키 또는 DB를 사용하여 저장된 토큰 기반으로 인증을 처리

 

remember-me 기능을 활성하고 인증받고 세션이 만료되면 실행되는 필터입니다.

세션이 만료되거나 무효화되어서 세션안에 있는 SecurityContext내의 인증 객체가 null일 경우 해당 필터가 작동합니다.

인증 객체가 null일 경우에 현재 사용자가 요청하는 request header에 remember-me cookie 값을 헤더에 저장한 상태로 왔을 때

이 필터가 접속한 사용자 대신에 인증처리를 시도합니다.

 

 

 

AnonymousAuthenticationFilter

사용자 정보가 인증되지 않았다면 익명 사용자 토큰을 반환

 

익명 사용자 필터는 이 필터가 호출되는 시점까지,

인증 시도를 하지 않고 권한도 없이 어떤 자원에 바로 접속을 시도하는 경우 실행됩니다.

 

이 필터는 인증되지 않은 사용자가 접근했을 때 annonymouseAuthenticationToken을 만들어서 SecurityContext 객체에 저장하는 역할을 합니다.

 

 

SessionManagementFilter

로그인 후 Session과 관련된 작업을 처리

 

이 필터는 조건이 현재 세션에 SecurityContext이 없거나 세션이 null인 경우에 동작됩니다.

해당 필터에서는 아래의 세 SessionInfo 등록, SessionFixation, ConcurrentSession을 진행합니다.


Register SessionInfo

사용자의 세션 정보가 등록합니다.

인증에 성공한 이후에 일반적으로 successHandler등 으로 다음 페이지 (가령 루트페이지)로 이동할텐데, 다시금 시큐리티 필터가 세션에 최종적으로 인증에 성공한 인증객체(SecurityContext)를 세션에 저장하는 처리를 응답 직전에 처리합니다.

처리 후 응답으로 해당 리소스로 이동하게 됩니다.

 

SessionFixation

세션 고정 보호로 인증에 성공한 시점에 새롭게 쿠키가 발급되며,

인증을 시도하기 전에 이전에 쿠키가 삭제되고 새로운 쿠키가 발급되도록 작동합니다.

 

ConcurrentSession

UsernamePasswordAuthenticationFilter인증을 진행하며 해당 필터가 동시에 검증된다고 했는데요.

사용자가 인증에 성공했다면 해당 사용자 계정으로 동시점에 세션이 존재하는지 확인합니다.

 

A라는 사람이 로그인을 하고 후에 B라는 사람이 같은 계정으로 로그인을 했다는 상황을 가정해봅시다.

먼저, A라는 사람은 성공적으로 인증을 받고 세션에 저장됩니다.

이후 B라는 사람이 인증을 시도하게 되면 해당 필터에서 동시 세션을 처리하게 되고,

Spring Security 설정 내역에 따라 세션 처리를 진행합니다.

 

이 때, 아래의 두 가지 전략에 따릅니다.

 

✔️ 현재 시점 사용자 인증 X

인증 자체를 실행하지 못하도록 인증관련 예외를 날립니다.

 

만약에, 이 시스템이 동일한 계정으로 생성할 수 있는 세션의 개수가 하나라고 지정되어있는 경우,

현재 사용자 인증 시도를 차단합니다.

SessionAuthenticationException를 던집니다.

 

✔️ 이전 사용자 인증 만료

현재 사용자는 인증을 계속 사용하고, 이전 사용자의 세션을 만료시킨다.

B 사용자를 인증하며 첫 번째 사용자의 세션을 만료하는 설정을 합니다.

이전 계정 세션을 session.expireNow를 false로 바꾸게 되고 이때 A 사용자는 리소스에 접근 시 ConcurrentSessionFilter에 걸려 더 이상 해당 인증을 사용하지 못하게 됩니다.

 

 

 

이제 남은 아래 두 필터는 인증 이후 자원에 접근할 때 가장 큰 역할을 하는 필터입니다.

 

ExceptionTranslationFilter

필터 체인 내에서 발생되는 인증, 인가 예외를 처리

 

인증, 인가 예외가 발생할 경우 실행됩니다.

인증과 인가에 대해 각각 AccessDeniedException, AuthenticationException를 던집니다.

 

chain.doFilter 로 바로 다음 필터로 넘기는 것을 try, catch로 감싸서 예외를 처리합니다.

 

 

 

FilterSecurityInterceptor

권한 부여와 관련한 결정을 AccessDecisionManager에게 위임해 권한부여 결정 및 접근 제어 처리

 

초기화 시 WebSecurityConfigurerAdapter의 설정에 대해 잠시 언급했는데요.

WebSecurityConfigurerAdapter을 구현한 설정 파일에 리소스 접근에 대한 설정을 했습니다.

그 때 설정했던 리소스 관련 설정들이 바로 HttpResourceSecurityConfig이며, 해당 필터에서 그 설정들을 처리하게 됩니다.

 

이 필터는 아래의 세 가지의 동작을 합니다.

 

Check Authenticated

인증 객체가 존재하는 지 확인합니다.

인증 객체가 없으면 즉시 인증 예외를 날리는데, 인증 객체가 없으면 인가 판단이 불가능하기 때문입니다.

인증 객체가 있다면, 아래의 동작을 수행합니다.

 

AccessDecisionManager

Spring Security의 인가는 AccessDecisionManager가 담당합니다.

 

인가 처리를 하며 AccessDecisionManager가 내부적으로 아래(accessDecisionVoter) 실행합니다.

Voter들을 사용하며 인가에 사용되는 여러 전략들이 존재합니다.

권한 없으면 AccessDeniedException을 날립니다.

 

accessDecisionVoter

접근하고자하는 자원의 승인과 거부를 판단합니다.

 

 

 

 

 

Spring Security의 인증, 인가 필터를 흐름 순으로 다뤘습니다.

요즘엔 너무 정신없이 살아서 이제야 Security 후속 포스팅을 냅니다 🥲

 

오타나 잘못된 내용을 댓글로 남겨주세요!

감사합니다 ☺️ 

 

 

반응형

Backend Software Engineer

Gyeongsun Park