-
[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의 구조
Spring Security Filter
- 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 앞에 있는 것을 확인할 수 있다.
- SecurityAutoConfiguration에서 ConditionalOnClass로 DefaultAuthenticationEventPublisher 클래스가 등록되어 있어 해당 클래스가 존재해야 SecurityAutoConfiguration Bean도 등록이 된다는 것을 알 수있다.
- DefaultAuthenticationEventPublisher는 Spring Security Starter를 의존성에 추가하기만 하면 만들어지는 클래스로 의존성을 추가하기만 한다면 IoC 컨테이너에 등록될 것이다
- 또한, SecurityAutoConfiguration에서 SpringBootWebSecurityConfiguration를 import 하고 있는 것을 확인할 수 있다.
- SpringBootWebSecurityConfiguration에서 WebSecurityEnablerConfiguration를 보면 @EnableWebSecurity이 있는 것을 확인할 수 있다.
- EnableWebSecurity 내부를 살펴보면 WebSecurityConfiguration 클래스를 import 하고 있는 것을 확인할 수 있다.
- WebSecurityConfiguration 클래스에서는 springSecurityFilterChain을 AbstractSecurityWebApplicationInitializer 클래스의 DEFAULT_FILTER_NAME인 "springSecurityFilterChain"으로 Bean을 등록하고 있는 것을 확인할 수 있다.
- 내부 로직을 살펴보면 Spring Security Filter Chain을 모두 불러와서 webSecurity 객체에 추가하고 있는 것을 확인할 수 있다.
- 최종적으로는 webSecurity.build() 함수를 실행하고 있는데 이는 WebSecurity의 performBuild() 함수를 실행하며 filterChainProxy를 반환되어 ApplicationContext에 Bean으로 등록되게 된다.
여기서 SecurityAutoConfiguration의 흐름을 정리해보자면 다음과 같다.
SecurityAutoConfiguration => SpringBootWebSecurityConfiguration => EnableWebSecurity => WebSecurityConfiguration => springSecurityFilterChain을 통해 filterChainProxy Bean 등록
이후, SecurityAutoConfiguration 클래스의 생성 과정을 마치고 다시 SecurityFilterAutoConfiguration로 돌아와 다음 로직을 실행하게 된다.
- securityFilterChainRegistration 메소드에서 DelegatingFilterProxyRegistrationBean 객체를 생성하고 있는데 어떤 값을 반환하고 있는 지 확인해보겠다.
- DelegatingFilterProxyRegistrationBean에서 AbstractFilterRegistrationBean를 상속받아 사용하고 있는 것을 확인할 수 있다.
- AbstractFilterRegistrationBean는 ServletContextInitializer를 구현하고 있으며 DelegatingFilterProxyRegistrationBean의 getFilter()가 만들어낸 필터를 Servlet Container에 필터로 등록한다.
- DelegatingFilterProxyRegistrationBean의 getFilter 메소드는 DelegatingFilterProxy 객체를 생성하고 있다.
- DelegatingFilterProxy도 doFilter() 함수를 실행하게 되는데, initDelegate 함수를 통해 위에서 ApplicationContext에 Bean으로 등록했던 springSecurityFilterChain FilterChainProxy를 불러오고 invokeDelegate 함수를 통해서 위임하여 Spring Security Filter Chain 등록을 마치게 된다.
모든 과정이 제대로 이뤄졌는지 확인하기 위해 아래 과정을 수행했다.
- 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를 보면 기본 필터의 순서가 제공된다.
- 사용자 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 객체를 생성하여 삽입하고 있다.
- SecurityContextRepository는 loadContext, saveContext, containsContext 메소드를 가지고 있다.
- HttpSessionSecurityContextRepository에서 해당 메소드들을 override하여 구현하고 있는 것을 확인할 수 있다.
익명 사용자
- 사용자로부터 요청오면 doFilter 메소드가 실행되게 된다. 코드를 보며 flow를 debug 해보도록 하겠다.
- 생성자에서 초기화했던 HttpSessionSecurityContextRepository인 repo 객체에서 loadContext를 통해 context를 불러오고 있다.
- 만약, httpSession에 SecurityContext 객체가 존재한다면 SecurityContext를 가져온다.
- 하지만, 익명 사용자의 경우 인증을 한 적이 없기 때문에 넘어가게 된다.
- 이후, generateNewContext() 함수를 통해 SecurityContext를 생성하고 SecurityContextPersistenceFilter에 반환하게 된다.
- 반환 후에는 setContext를 통해 ThreadLocal에 저장되며 다음 필터인 AnonymousAuthenticationFilter로 이동한다.
- AnonymousAuthenticationFilter에서 SecurityContext에 저장되어 있는 Authentication 객체가 없기 때문에 createAuthentication()을 통해서 AnonymousAuthenticationToken을 생성한다.
- AnonymousAuthenticationFilter이 끝나게 되면 다시 SecurityContextPersistenceFilter로 돌아와 finally 과정을 수행하게 된다.
- clearContext()를 통해 ThreadLocal의 SecurityContext를 삭제하고 saveContext()를 통해 사용자 응답 Session에 Authentication을 추가한다.
인증을 시도하는 사용자
- Form 로그인으로 요청을 보낼 경우도 마찬가지로 Session에 SecurityContext의 Authentication은 null일 것이다.
- 따라서, contextBeforeChainExecution의 Authentication도 null일 것이다.
- 인증 요청의 경우 chain.doFilter()의 경우 AbstractAuthenticationProcessingFilter로 이동하는데 fromLogin() 옵션을 주게 되면 위 필터를 상속받는 UsernamePasswordAuthenticationFilter로 처리하게 된다.
- 상속받고 있으므로 AbstractAuthenticationProcessingFilter의 doFilter가 실행되고 attempAuthentication()가 실행하게 된다.
- UsernamePasswordAuthenticationFilter는 해당 메소드를 override 해서 처리하고 있으므로 해당 코드를 살펴보겠다.
- 코드를 보면 username, password를 가져와서 UsernamePasswordAuthenticationToken을 생성하고 AuthenticationManager을 구현한 객체에 인증을 위임하여 Authentication 결과를 반환한다.
- AuthenticationManager도 AuthenticationProvider에게 인증 처리를 위임하고 그 결과를 다시 Filter에게 반환하게 된다.
- AuthenticationManager는 인터페이스이고 기본적으로 제공되는 구현체는 ProviderManager이다. 여러 개의 AuthenticationProvider를 가지고 있으며 이 중에서 요청을 처리할 수 있는 Provider가 authenticate()를 수행한다.
- DaoAuthenticationProvider가 적용되어 있는 것을 알 수 있는데 어떤 Provider가 처리할 수 있는 지 판단하는 부분이 바로 supports() 메소드이다.
- supports는 필터에서 보낸 Authentication 객체를 현재의 AuthenticationProvider가 처리할 수 있는 지 판단한다.
- DaoAuthenticationProvider를 보면 AbstractUserDetailsAuthenticationProvider를 상속하고 있으며 해당 클래스안에는 supports 메소드가 있다.
- 넘어온 파라미터 값이 UsernamePasswordAuthenticationToken인지를 확인하고 값을 넘겨주고 있다.
- 다시 ProviderManager로 돌아가서 이렇게 검증을 완료한 토큰의 경우 authentication() 메소드를 실행하게 된다. 해당 메소드도 마찬가지로 AbstractUserDetailsAuthenticationProvider에 구현되어 있는 것을 확인할 수 있다.
- AbstractUserDetailsAuthenticationProvider에서 authenticate를 보면 user 정보를 가져올 때 retrieveUser() 메소드를 통해서 가져오는 것을 확인할 수 있다.
- 해당 메소드는 AuthenticationProvider에서 override해서 구현하고 있으니 DaoAuthenticationProvider에서 살펴보도록 하겠다.
- UserDetailsService에서 username을 기반으로 유저 정보를 찾아오는 것을 확인할 수 있다.
- UserDetailsService는 loadUserByUsername 메소드를 통해 UserDetails를 가져오기 위한 인터페이스이다.
- 개발자가 따로 구현하지 않는다면 InMemoryUserDetailsManager라는 클래스를 등록한다.
- 해당 클래스는 Map을 통해 유저 정보를 등록하고 관리하며 UserDetailsManager의 구현체이다.
- 또한, UserDetailsManager도 UserDetailsService를 상속하고 있어서 DaoAuthenticationProvider에서 사용 가능하다.
- 해당 user 정보를 찾으면 AbstractUserDetailsAuthenticationProvider로 UserDetails 객체를 반환하고 있다.
- 올바른 UserDetails가 넘어오면 createSuccessAuthentication 메소드에 Authentication 객체와 UserDetails 객체를 담아 호출한다.
- authenticated() 메소드를 통해 인증이 되었다는 의미의 UsernamePasswordAuthenticationToken을 생성하여 최종 반환값이 AbstractAuthenticationProcessingFilter에게 전달된다.
- Authentication이 정상적으로 반환되면 successfulAuthentication() 메소드를 호출하고 있는 것을 알 수 있다.
- successfulAuthentication() 메소드로 전달되어 SecurityContext에 Authentication을 삽입하고 SecurityContextHolder에 저장되며 securityContextRepository의 saveContext에 의해 Session에 저장된다.
인증에 성공한 사용자
- 동일하게 SecurityContextPersistenceFilter에 들어오게 되고 HttpSessionSecurityContextRepository의 loadContext() 메소드가 실행된다.
- HttpSession에 SecurityContext가 존재하여 readSecurityContextFromSession 메소드를 통해 읽어오고 반환하게 되며 다음 필터를 실행하게 된다.
다음 포스팅에서는 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