주말이라는 것은… 사람을 참 나태하게 만들기도
활력을 공급하기도 하는 것 같다.

image

토요일까지 Spring Security를 공부하다가 번아웃이와
일요일에 너무 게으르게 누워만 있었더니
유튜브 나태지옥에 다시 빠져버렸다.


저번주에 Spring Security를 사용하는 예제 코드와
간단한 흐름을 알아보았다.
그중 Filter라는 것들을 배웠고 Spring Security도
Filter로 구성된 FilterChain으로 구동하는 것도 배웠다.

오늘은 그 중에서 인증관련된 Filter를 자세히 알아보자

Spring Security 인증

Filter 흐름 복습

저번주에 공부했었던 (Spring Security 기초)의 흐름을 다시한번 짚고 넘어가자

image

클라이언트에서 요청이오면
Filter들의 모임인 FilterChain을 거쳐 하나씩 실행한다.
그 중 FilterChainProxy 클래스로 부터 보안을 위한 작업 필터모음인
SecurityFilterChain를 수행하게된다.

여기서 SecurityFilterChain도 Filter들로 이루어져있고
아래의 사진이 SecurityFilterChain의 대략적인 구조이다.

image
hyozkim.log - 출처

사진에서 보면 알 수 있듯이
SecurityFilterChain의 구조는

SecurityContextPersistenceFilter  
LogoutFilter  
UsernamePasswordAuthenticationFilter  
...  

등 여러가지 필터를가진 형태로 존재한다.

모든 Filter의 구조를 알고 이해한다면 분명 SpringSecurity에 대한
이해도가 깊어질 것이자만, 현재 공부를 시작한지 얼마안된
내가 전부를 이해하기에는 사실 무리가 있는 것 같다고 생각이 들었다.

이 중에서도 인증과 관련된 UsernamePasswordAuthenticationFilter의
인증 처리 흐름을 이해해보려한다. 해당하는 부분을 어제 코드로 구현하기도 했고
흐름을 알아야 SpringSecurity를 기본적인 내용을 구현하는데 무리가 없을 것 같다.


Security Filter 확인 방법?

위에서 얘기했듯이 여러 Filter들을 거쳐서
인증과 권한에 대한 수행을 SpringSecurity가 진행해준다.
실제로 어떠한 Filter를 거쳐왔는지 확인을 할 수 있는 애노테이션이 있고
디버그 용도로 매우 유용하게 쓰일 것 같다.

@Configuration
@EnableWebSecurity(debug = true) // 디버그 설정
public class SecurityConfigurationV2 {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .formLogin()
                .loginPage("/auths/login-form") 
                .loginProcessingUrl("/process_login")
                .failureUrl("/auths/login-form?error")
                .and()
                .authorizeHttpRequests() 
                .anyRequest() 
                .permitAll();
        return http.build();
    }
}

@EnableWebSecurity(debug = true) 어노테이션을
이용해 현재 요청에 이용된 Filter의 목록이 조회가 가능하다.

애플리케이션을 실행시키고
formLogin 방식으로 요청을 보내보았다.

그리고 콘솔을 확인해보면

Security filter chain: [
  DisableEncodeUrlFilter
  WebAsyncManagerIntegrationFilter
  SecurityContextPersistenceFilter
  HeaderWriterFilter
  LogoutFilter
  UsernamePasswordAuthenticationFilter
  RequestCacheAwareFilter
  SecurityContextHolderAwareRequestFilter
  AnonymousAuthenticationFilter
  SessionManagementFilter
  ExceptionTranslationFilter
  AuthorizationFilter
]

첫번쨰로 요청에 대한 정보들과
다음으로는 Security filter chain에 Filter 목록들이
나오는 것을 확인할 수 있다.

위에서 볼 수 있듯이 우리가 알아보려고하는
UsernamePasswordAuthenticationFilter 클래스도
콘솔로 확인이 가능하다.


인증 처리

이제 본격적으로 인증이 어떠한 과정으로
진행되는지 처리 순서에 대한 흐름을 알아보자.

image

(1). 클라이언트에서 로그인 요청이 오면, SecurityFilterChain을 거치게된다.
그 중 인증에 관한 부분은 UsernamePasswordAuthenticationFilter 클래스이다.


(2). Authentication 객체를 생성한다.
여기서 로그인할때 받아온 Username과 Password를 이용해
Authentication의 구현체인 UsernamePasswordAuthenticationToken
클래스를 이용해 Authentication 객체를 만든다.
(❗️Authentication 객체는 인증이 완료되지 않음)


(3). Authentication 객체를 AuthenticationManager로 전달한다.
여기서 구현체인 ProviderManager가 인증이라는 작업의 총괄 매니저다.
총괄은 하지만 실제 인증처리는 AuthenticationProvider에게 위임한다.
(❗️Authentication 객체는 인증이 완료되지 않음)


(4). Authentication 객체를 AuthenticationProvider로 전달한다.
ProviderManager가 전달해준 Authentication객체로 인증하는 작업을 진행한다.
인증을 구현하는 방법은 2가지 정도가 있다.

  1. AuthenticationProvider를 상속받아 구현
  2. UserDetailsService를 상속받아 구현

(Spring Security 기초)에서 우리는 UserDetailsService
상속받아 직접 클래스를 구현했었다.

여기서 1번방법으로 구현하면 UserDetailsService` 부분은
사용하지 않고 구현을 할 수 있다. 구현할때 중요한 부분은
직접 Credentials과 Password를 인증과정 로직을 구현해야한다는 점이다.

하지만 어제 우리가 구현했던 2번방법으로 구현하면
AuthenticationProvider를 구현하지 않아도된다.
DaoAuthenticationProvider를 구현 클래스로 사용하기 때문이다. 실제로 호출 흐름을 본다면,

  1. ProviderManager 클래스의 .authenticate(); 메서드 호출
  2. AuthenticationProvider 인터페이스의 AbstractUserDetailsAuthenticationProvider
    .authenticate(); 메서드를 구현했음.
  3. .authenticate(); 메서드안에서 인증 처리를 위한 retrieveUser(); 호출
    DaoAuthenticationProvider 클래스가 .retrieveUser();를 구현했음
  4. .retrieveUser(); 메서드에서 UserDetailsService 클래스의 .loadUserByUsername()
    메서드 호출 .loadUserByUsername(); 메서드는 우리가 구현해야하는 부분이다.

(❗️Authentication 객체는 인증이 완료되지 않음)


(5). 즉, 위에서 얘기했던 DaoAuthenticationProvider 클래스가
UserDetailsService를 이용해 UserDetails를 객체를 조회 한다.

실제로 어제 예제에서 UserDetails를 상속받아
username, password, 권한정보를 객체로 만들어 구현하였고
해당객체를 UserDetailsService를 이용해 반환하는 프로그램도 작성했다.
(❗️Authentication 객체는 인증이 완료되지 않음)


(6). 즉, Credential 저장소라는 뜻은 DB의 암호화된 비밀번호를 가져온다는 뜻이다.
쉽게 말하자면 (5)번을 통해 호출된 UserDetailsService에서
loadUserByUsername(String username); 메서드를 이용해
우리는 어떤 username으로 접근하는지 알 수 있으며, 해당 정보로 DB를 조회해
싫제로 동일한 username이 존재할 경우 해당 테이블의 정보를 가져올 수 있다.
즉, 권한정보, 암호화된 비밀번호 등등 사용자 정보를 가져올 수 있다는 뜻이다.


(7). 이제 우리는 (6)번을 통해서 조회한 데이터로 인증을 비교할
UserDetails 객체를 만들 수 있다. UserDetails 객체는
username,password,권한정보를 가지고 있으면 객체로 만들 수 있다.


(8). 만들어진 UserDetails 객체는, 맨처음 호출했던
DaoAuthenticationProvider 클래스에 반환하게 되어진다.
아까 얘기했듯이 AuthenticationProvider를 구현한 클래스로
인증을 처리해주는 클래스이다.


(9). UserDetails 정보를 이용해 DaoAuthenticationProvider 클래스가
.mitigateAgainstTimingAttack(); 메서드를 호출해 인증을 처리한다.
인증처리가 정상적으로 이루어졌을 경우에
그림과 같이 인증정보인 Collection<GrantedAuthority>가 추가된
Authentication 객체를 반환하게 된다.
(⭕️Authentication 객체는 인증이 완료됨)


(10). 이제 인증이 완료된 객체를 호출했던
ProviderManager로 반한된다.
(⭕️Authentication 객체는 인증이 완료됨)


(11). 마찬가지로 처음에 호출을 했던
UsernamePasswordAuthenticationFilter 클래스로
인증이완료된 객체가 반환되게 되어진다.
(⭕️Authentication 객체는 인증이 완료됨)


(12). 최종적으로 인증이 완료된 Authentication 객체를
SecurityContextHolder를 이용해 SecurityContext에 저장을하게 된다.
SecurityContext의 세션 정책에 따라서
HttpSession에 저장되어 사용자의 인증 상태를 유지하기도 하고
HttpSession을 생성하지 않고 무상태를 유지하기도 한다.



이렇게 오늘은 SecurityFilterChain에서
어떻게 인증이 이루어지는지 알아보았다.

오늘 정말 파급적인 효과로 코드에 대해 이해를 했다.
아키텍쳐를 그려가면서, 실제 코드들이 호출되서
실행되는 것을 하나하나 따라가보면서 순서에 대한 흐름을 글로 정리해봤다.

이렇게 코드를 따라가면서, 파악한 내용을 적고 하다보니까
어디서 부터 시작되었고, 어떻게 객체를 돌려받는지가
파악되다보니까 어느정도 인증처리에 대한 흐름은 잡힌 것 같다.

오늘 공부는 여기서 끝 !!


오늘의 커피량: ☕️ ☕️
오늘의 점심: 된장찌개, 라면, 밥