-
[Spring boot] Spring Security + JWT + Redis (1/3)프로젝트/Share Your Trip 2024. 2. 5. 21:33
Spring Security 에 대해서 왜 궁금했을까 ❓
- Share Your Trip 프로젝트에서 여행지를 공유할 수 있는 커뮤니티 게시판이 존재했다. 이러한 기능에서 불필요한 글을 작성하거나 악성 유저의 글을 관리할 관리자가 필요했다.
- Spring에서는 Spring Secuity를 통해서 인증과 권한을 관리할 수 있었다. 이를 통해, 사용자와 관리자를 인증하고 권한을 분리하여 보안성을 강화해보려고 한다.Spring Security의 구조
Architecture :: Spring Security
The Security Filters are inserted into the FilterChainProxy with the SecurityFilterChain API. Those filters can be used for a number of different purposes, like authentication, authorization, exploit protection, and more. The filters are executed in a spec
docs.spring.io
Spring Security Filter
ApplicationFilterChain - Servlet Container가 관리하는 ApplicationFilterChain에서 ApplicationContext에 등록된 Bean Filter를 사용하는 것은 불가능하여 DelegatingFilterProxy를 이용하여 Servlet Container와 ApplicationContext를 연결한다.
- DelegatingFilterProxy에 Bean Filter인 FilterChainProxy를 등록하고 Bean으로 등록된 SecurityFilterChain을 수행한다.
- SecurityFilterChain은 Security Filter들을 가져오고 필터링을 진행한다.
Security Filter Chain
- Http Request가 들어오면 AuthenticationFilter가 요청을 잡아서 AuthenticationManager에게 위임한다.
- AuthenticationManager는 AuthenticationProvider에게 인증 정보를 요청한다.
- AuthenticationProvider에서 UserDetailService를 이용하여 DB에서 유저 정보를 조회하고 UserDetails 결과를 반환한다.
- SecurityContextHolder에 해당 객체를 저장하고 다음 필터를 통과하게 된다.
- 모든 필터를 통과한다면 최종적으로 Controller에서 접근 및 활용한다.
Spring Security 내부 로직
- 사용자가 ID, PW를 전송하면, AbstractAuthenticationProcessingFilter가 받아 Authentication 객체를 생성한다.
- Authentication 객체를 AuthenticationManager로 전송한다.
- 인증에 실패한다면, SecurityContextHolder에서 유저의 정보가 지워지고 RememberMeService.joinFail()이 실행된다. 그리고 최종적으로 AuthenticationFailureHandler가 실행하게 된다.
- 인증에 성공한다면, SessionAuthenticationStrategy가 새로운 로그인이 된 것을 알리고 Authentication 객체가 SecurityContextHolder에 저장된다. 이후에 SecurityContextPersistenceFilter가 SecurityContext를 HttpSession에 저장하면서 로그인 세션 정보가 저장됩니다.
- 이후에는 RememberMeServices.loginSuccess()가 실행된다. 그리고 ApplicationEventPublisher가 InteractiveAuthenticationSuccessEvent를 발생시키고 AuthenticationSuccessHandler가 실행된다.
디버깅을 통한 Spring Security 구조 파악
Spring Security Filter
- SecurityFilterAutoConfiguration를 보면 @AutoConfiguration에 after 옵션을 주어 SecurityAutoConfiguration 클래스가 로드된 이후에 실행된다고 명시되어 있다.
- spring-boot-autoconfigure에 등록된 설정을 보면 SecurityAutoConfiguration가 SecurityFilterAutoConfiguration 앞에 있는 것을 확인할 수 있다.
spring-boot-autoconfigure - SecurityAutoConfiguration에서 ConditionalOnClass로 DefaultAuthenticationEventPublisher 클래스가 등록되어 있어 해당 클래스가 존재해야 SecurityAutoConfiguration Bean도 등록이 된다는 것을 알 수있다.
- DefaultAuthenticationEventPublisher는 Spring Security Starter를 의존성에 추가하기만 하면 만들어지는 클래스로 의존성을 추가하기만 한다면 IoC 컨테이너에 등록될 것이다
- 또한, SecurityAutoConfiguration에서 SpringBootWebSecurityConfiguration를 import 하고 있는 것을 확인할 수 있다.
SecurityAutoConfiguration - SpringBootWebSecurityConfiguration에서 WebSecurityEnablerConfiguration를 보면 @EnableWebSecurity이 있는 것을 확인할 수 있다.
SpringBootWebSecurityConfiguration - EnableWebSecurity 내부를 살펴보면 WebSecurityConfiguration 클래스를 import 하고 있는 것을 확인할 수 있다.
EnableWebSecurity - WebSecurityConfiguration 클래스에서는 springSecurityFilterChain을 AbstractSecurityWebApplicationInitializer 클래스의 DEFAULT_FILTER_NAME인 "springSecurityFilterChain"으로 Bean을 등록하고 있는 것을 확인할 수 있다.
- 내부 로직을 살펴보면 Spring Security Filter Chain을 모두 불러와서 webSecurity 객체에 추가하고 있는 것을 확인할 수 있다.
WebSecurityConfiguration - 최종적으로는 webSecurity.build() 함수를 실행하고 있는데 이는 WebSecurity의 performBuild() 함수를 실행하며 filterChainProxy를 반환되어 ApplicationContext에 Bean으로 등록되게 된다.
WebSecurity 여기서 SecurityAutoConfiguration의 흐름을 정리해보자면 다음과 같다.
SecurityAutoConfiguration => SpringBootWebSecurityConfiguration => EnableWebSecurity => WebSecurityConfiguration => springSecurityFilterChain을 통해 filterChainProxy Bean 등록
이후, SecurityAutoConfiguration 클래스의 생성 과정을 마치고 다시 SecurityFilterAutoConfiguration로 돌아와 다음 로직을 실행하게 된다.
- securityFilterChainRegistration 메소드에서 DelegatingFilterProxyRegistrationBean 객체를 생성하고 있는데 어떤 값을 반환하고 있는 지 확인해보겠다.
SecurityFilterAutoConfiguration - DelegatingFilterProxyRegistrationBean에서 AbstractFilterRegistrationBean를 상속받아 사용하고 있는 것을 확인할 수 있다.
DelegatingFilterProxyRegistrationBean - AbstractFilterRegistrationBean는 ServletContextInitializer를 구현하고 있으며 DelegatingFilterProxyRegistrationBean의 getFilter()가 만들어낸 필터를 Servlet Container에 필터로 등록한다.
AbstractFilterRegistrationBean - DelegatingFilterProxyRegistrationBean의 getFilter 메소드는 DelegatingFilterProxy 객체를 생성하고 있다.
DelegatingFilterProxyRegistrationBean - DelegatingFilterProxy도 doFilter() 함수를 실행하게 되는데, initDelegate 함수를 통해 위에서 ApplicationContext에 Bean으로 등록했던 springSecurityFilterChain FilterChainProxy를 불러오고 invokeDelegate 함수를 통해서 위임하여 Spring Security Filter Chain 등록을 마치게 된다.
DelegatingFilterProxy - doFilter DelegatingFilterProxy - initDelegate DelegatingFilterProxy - invokeDelegate 모든 과정이 제대로 이뤄졌는지 확인하기 위해 아래 과정을 수행했다.
ApplicationFilterChain - doFilter에 BreakPoint를 걸고 Http Request를 전송한다.
- Servlet FilterChain인 ApplicationFilterChain에서 2번째로 springSecurityFilterChain을 실행하는 것을 확인할 수 있다.
- filter를 보면 DelegatingFilterProxyRegistrationBean이 등록되어 있으며 열어보면 springSecurityFilterChain을 targetBeanName을 가지고 있는 것을 확인할 수 있다.
- delegate 속성을 보면 FilterChainProxy에 위임하고 있는 것을 확인할 수 있다.
- filterChains를 살펴보면 Spring Security에 등록한 필터들이 있는 것을 확인할 수 있다.
Security Filter Chain
Security Filter Chain의 인증 및 인가 과정을 모든 필터에서 예외가 발생하지 않으면 보안을 통과하게 된다.
- Spring Security Filter는 33개가 존재하며 인증 및 인가 과정을 살펴보려고 한다.
- 또한, Filter의 Order가 존재하여 순서가 중요한데, HttpSecurity의 FilterOrderRegistration를 보면 기본 필터의 순서가 제공된다.
FilterOrderRegistration Persisting Authentication :: Spring Security
The first time a user requests a protected resource, they are prompted for credentials. One of the most common ways to prompt for credentials is to redirect the user to a log in page. A summarized HTTP exchange for an unauthenticated user requesting a prot
docs.spring.io
- 사용자 Request는 FilterChainProxy를 통해 Spring Security Filter로 진입하고 Filter Chain들을 하나씩 수행한다.
- 실질적인 사용자 인증은 AuthenticationFilter에서 진행되는데 Authentication 객체를 관리할 공간인 SecurityContext가 필요하게 된다.
- SecurityContext는 AuthenticationFilter를 거치기전에 SecurityContextPersistenceFilter를 통해 관리된다.
SecurityContext 객체의 생성, 저장, 조회
- 익명 사용자
- SecurityContext 객체를 생성하고 SecurityContextHolder에 저장
- AnonymousAuthenticationFilter에서 AnonymousAuthenticationToken 객체를 SecurityContext에 저장
- 인증 시
- AnonymousAuthenticationFilter에서 AnonymousAuthenticationToken 객체를 SecurityContext에 저장
- UsernamePasswordAuthenticationFilter에서 인증 성공 후, SecurityContext에 UsernamePasswordAuthentication객체를 SecurityContext에 저장
- 인증이 최종적으로 완료되면 Session에 SecurityContext를 저장
- 인증 후
- Session에서 SecurityContext를 가져와 SecurityContextHolder에 저장
- SecurityContext안에 Authentication이 존재하면 인증을 계속 유지
- 최종 응답 시
- SecurityContextHolder안의 Security를 제거
- SecurityContextHolder.clearContext()를 호출하여 인증 정보 초기화
- SecurityContextPersistenceFilter의 생성자를 살펴보면 SecurityContextRepository 인터페이스의 구현체로 HttpSessionSecurityContextRepository 객체를 생성하여 삽입하고 있다.
SecurityContextPersistenceFilter - SecurityContextRepository는 loadContext, saveContext, containsContext 메소드를 가지고 있다.
- HttpSessionSecurityContextRepository에서 해당 메소드들을 override하여 구현하고 있는 것을 확인할 수 있다.
익명 사용자
- 사용자로부터 요청오면 doFilter 메소드가 실행되게 된다. 코드를 보며 flow를 debug 해보도록 하겠다.
- 생성자에서 초기화했던 HttpSessionSecurityContextRepository인 repo 객체에서 loadContext를 통해 context를 불러오고 있다.
SecurityContextPersistenceFilter - 만약, httpSession에 SecurityContext 객체가 존재한다면 SecurityContext를 가져온다.
- 하지만, 익명 사용자의 경우 인증을 한 적이 없기 때문에 넘어가게 된다.
- 이후, generateNewContext() 함수를 통해 SecurityContext를 생성하고 SecurityContextPersistenceFilter에 반환하게 된다.
- 반환 후에는 setContext를 통해 ThreadLocal에 저장되며 다음 필터인 AnonymousAuthenticationFilter로 이동한다.
HttpSessionSecurityContextRepository loadContext() - AnonymousAuthenticationFilter에서 SecurityContext에 저장되어 있는 Authentication 객체가 없기 때문에 createAuthentication()을 통해서 AnonymousAuthenticationToken을 생성한다.
AnonymousAuthenticationFilter AnonymousAuthenticationToken - AnonymousAuthenticationFilter이 끝나게 되면 다시 SecurityContextPersistenceFilter로 돌아와 finally 과정을 수행하게 된다.
- clearContext()를 통해 ThreadLocal의 SecurityContext를 삭제하고 saveContext()를 통해 사용자 응답 Session에 Authentication을 추가한다.
SecurityContextPersistenceFilter 인증을 시도하는 사용자
- Form 로그인으로 요청을 보낼 경우도 마찬가지로 Session에 SecurityContext의 Authentication은 null일 것이다.
- 따라서, contextBeforeChainExecution의 Authentication도 null일 것이다.
- 인증 요청의 경우 chain.doFilter()의 경우 AbstractAuthenticationProcessingFilter로 이동하는데 fromLogin() 옵션을 주게 되면 위 필터를 상속받는 UsernamePasswordAuthenticationFilter로 처리하게 된다.
- 상속받고 있으므로 AbstractAuthenticationProcessingFilter의 doFilter가 실행되고 attempAuthentication()가 실행하게 된다.
- UsernamePasswordAuthenticationFilter는 해당 메소드를 override 해서 처리하고 있으므로 해당 코드를 살펴보겠다.
AbstractAuthenticationProcessingFilter - 코드를 보면 username, password를 가져와서 UsernamePasswordAuthenticationToken을 생성하고 AuthenticationManager을 구현한 객체에 인증을 위임하여 Authentication 결과를 반환한다.
UsernamePasswordAuthenticationFilter - AuthenticationManager도 AuthenticationProvider에게 인증 처리를 위임하고 그 결과를 다시 Filter에게 반환하게 된다.
- AuthenticationManager는 인터페이스이고 기본적으로 제공되는 구현체는 ProviderManager이다. 여러 개의 AuthenticationProvider를 가지고 있으며 이 중에서 요청을 처리할 수 있는 Provider가 authenticate()를 수행한다.
ProviderManager - DaoAuthenticationProvider가 적용되어 있는 것을 알 수 있는데 어떤 Provider가 처리할 수 있는 지 판단하는 부분이 바로 supports() 메소드이다.
- supports는 필터에서 보낸 Authentication 객체를 현재의 AuthenticationProvider가 처리할 수 있는 지 판단한다.
ProviderManager - DaoAuthenticationProvider를 보면 AbstractUserDetailsAuthenticationProvider를 상속하고 있으며 해당 클래스안에는 supports 메소드가 있다.
- 넘어온 파라미터 값이 UsernamePasswordAuthenticationToken인지를 확인하고 값을 넘겨주고 있다.
DaoAuthenticationProvider AbstractUserDetailsAuthenticationProvider - 다시 ProviderManager로 돌아가서 이렇게 검증을 완료한 토큰의 경우 authentication() 메소드를 실행하게 된다. 해당 메소드도 마찬가지로 AbstractUserDetailsAuthenticationProvider에 구현되어 있는 것을 확인할 수 있다.
- AbstractUserDetailsAuthenticationProvider에서 authenticate를 보면 user 정보를 가져올 때 retrieveUser() 메소드를 통해서 가져오는 것을 확인할 수 있다.
- 해당 메소드는 AuthenticationProvider에서 override해서 구현하고 있으니 DaoAuthenticationProvider에서 살펴보도록 하겠다.
AbstractUserDetailsAuthenticationProvider - UserDetailsService에서 username을 기반으로 유저 정보를 찾아오는 것을 확인할 수 있다.
DaoAuthenticationProvider - UserDetailsService는 loadUserByUsername 메소드를 통해 UserDetails를 가져오기 위한 인터페이스이다.
- 개발자가 따로 구현하지 않는다면 InMemoryUserDetailsManager라는 클래스를 등록한다.
- 해당 클래스는 Map을 통해 유저 정보를 등록하고 관리하며 UserDetailsManager의 구현체이다.
- 또한, UserDetailsManager도 UserDetailsService를 상속하고 있어서 DaoAuthenticationProvider에서 사용 가능하다.
InMemoryUserDetailsManager InMemoryUserDetailsManager UserDetailsManager - 해당 user 정보를 찾으면 AbstractUserDetailsAuthenticationProvider로 UserDetails 객체를 반환하고 있다.
- 올바른 UserDetails가 넘어오면 createSuccessAuthentication 메소드에 Authentication 객체와 UserDetails 객체를 담아 호출한다.
AbstractUserDetailsAuthenticationProvider authenticate() - authenticated() 메소드를 통해 인증이 되었다는 의미의 UsernamePasswordAuthenticationToken을 생성하여 최종 반환값이 AbstractAuthenticationProcessingFilter에게 전달된다.
AbstractUserDetailsAuthenticationProvider UsernamePasswordAuthenticationToken의 authenticate에 의해 호출된 생성자 - Authentication이 정상적으로 반환되면 successfulAuthentication() 메소드를 호출하고 있는 것을 알 수 있다.
AbstractAuthenticationProcessingFilter - successfulAuthentication() 메소드로 전달되어 SecurityContext에 Authentication을 삽입하고 SecurityContextHolder에 저장되며 securityContextRepository의 saveContext에 의해 Session에 저장된다.
AbstractAuthenticationProcessingFilter 인증에 성공한 사용자
- 동일하게 SecurityContextPersistenceFilter에 들어오게 되고 HttpSessionSecurityContextRepository의 loadContext() 메소드가 실행된다.
SecurityContextPersistenceFilter - HttpSession에 SecurityContext가 존재하여 readSecurityContextFromSession 메소드를 통해 읽어오고 반환하게 되며 다음 필터를 실행하게 된다.
HttpSessionSecurityContextRepository 다음 포스팅에서는 SpringSecurity에서 JWT 인증 방식을 도입하여 커스터 마이징하고 구현하는 과정을 소개해 보겠다.
'프로젝트 > Share Your Trip' 카테고리의 다른 글
[Spring boot] Spring Security + JWT + Redis (3/3) (0) 2024.02.07 [Spring boot] Spring Security + JWT + Redis (2/3) (0) 2024.02.06 [Spring boot] @Builder (0) 2024.01.14 [Spring boot] JWT(JSON Web Token) (0) 2024.01.12 [Spring boot] Password 암호화 (0) 2024.01.11