[Spring Security] 내부 흐름 분석 - 인증 성공

minseok Kim·2025년 2월 4일

Spring Security

목록 보기
5/15

지난 포스트에 이어 이번 포스트에서는 사용자가 로그인 폼에 올바른 자격 증명을 했을 때 내부 흐름을 파악해보도록 하겠다.


AbstractAuthenticationProcessingFilter

사용자가 로그인 할 때 거치는 Filter 중 하나로, 추상 클래스이며 구현 클래스로 UsernamePasswordAuthenticationFilter가 있다.


해당 필터의 doFilter를 확인해보면, try문 바로 아래 attemptAuthentication 메소드가 존재하고, 해당 메소드에서 실제 인증을 시도한다. 해당 메소드의 실제 구현은 구현 클래스인 UsernamePasswordAuthentiationFilter 안에 존재한다.


코드 분석

로그인 시 어떤 순서로 필터가 작동되는지 알아봤으니 이번에도 디버그 모드로 어떤 클래스들이 어떤 값을 가지고 내부적으로 동작하는지 확인해보았다.
AbstractAuthenticationProcessingFilter의 doFilter의 첫 번째 줄, UsernamePasswordAuthenticationFilter의 attemptAuthentication 메소드의 첫 번째 줄에 중단점을 설정하고 로그인 과정을 진행하였다.


![](https://velog.velcdn.com/images/bingseok/post/0c4296c1-243a-42b8-b887-ff88703f7760/image.png)

로그인 버튼을 누르면, AbstractAuthenticationProcessingFilter의 중단점에서 중단된다. 해당 필터는 이 요청이 인증이 필요한지 여부를 결정하는 역할을 한다. 인증이 필요하다면, 이 때는 UsernamePasswordAuthenticationFilter 내부에 있는 attemptAuthentication() 메소드를 호출한다.

AbstractAuthenticationProcessingFilter에는 attemptAuthentication() 메소드는 물론, 인증이 성공했을 때 호출되는 successfulAuthentication(), 인증이 실패했을 때 호출되는 unsuccessfulAuthentication() 메소드 등 다른 여러 메소드들이 존재하는데, 해당 필터는 Security Context를 UserDetail로 채우는 역할도 한다.

이후 중단점을 해제하고 UsernamePasswordAuthenticationFilter로 이동하였다.


해당 메소드를 보면, HttpServletRequest와 HttpServletResponse의 형태로 입력을 받고 해당 필터는 이 request에서 사용자가 입력한 자격 증명을 추출한다.

77번 째 줄에서 obtainUsername() 메소드는 username을 받는데, obtainUsername()을 보면 로그인 페이지 안에서 usernameParameter라는 이름의 매개변수를 찾는 것을 확인할 수 있다.

usernameParameter는 아래와 같이 선언되어있고,

usernameParameter 매개변수는 username, passwordParameter 매개변수는 password로 되어있는 것을 확인할 수 있다.

코드를 계속 실행해보면 로그인 폼에 입력한 username, password를 잘 추출했음을 확인할 수 있다.

그 이후의 코드를 보면 UsernamePasswordAuthenticationToken 객체를 생성하는데, 이는 앞서 내부 흐름에서 확인한 Authentication 객체를 만드는 과정이다.
Authentication은 인터페이스이므로, 이를 extend한 클래스인 UsernamePasswordAuthenticationToken을 통해 Authentication 객체를만드는 것이다.


각 클래스를 따라가보면


UsernamePasswordAuthentcationToken은 AbstractAuthenticationToken을 extend하고, AbstractAuthentication은 Authentication을 implement 하는 것을 알 수 있다.

따라서, unauthenticated() 메소드를 통해 UsernamePasswordAuthenticationToken에 사용자 이름과 자격 증명을 채울 것이다.

UsernamePasswordAuthenticationToken의 생성자를 열어보면

setAuthenticated(false);를 볼 수 있는데, 해당 메소드를 찾아보면

isAuthenticated라는 boolean을 false로 하는 것을 볼 수 있다.
초기 인증을 이제 막 시작했기 때문에, 인증을 했나를 나타내는 변수의 초기값을 false로 하는 것이다.

디버그를 계속 해보면

마지막으로 AuthenticationManager를 호출하고 AuthenticationManager 내부에는 authenticate() 메소드가 있다.

이제 authenticate() 메소드를 디버그 해보도록 하겠다.
authenticate()로 이동하니 ProviderManager 클래스로 이동하였다.

ProviderManager는 AuthenticationManager의 구현 클래스인 것을 확인할 수 있다. AuthenticationManager 또한 인터페이스이므로 해당 인터페이스를 구현하는 클래스를 필요로 한다.

해당 클래스의 authenticate() 메소드를 보면, 아래와 같은 for문이 있는데

이는 현재 프로젝트에서 적용 가능한 모든 AuthenticationProvider를 반복해서 확인하는 역할을 한다.
ProviderManager(AutheticationManager)는 provider라는 객체 이름을 사용하여 각 provider의 authenticate() 메소드를 호출함으로써 모든 provider를 확인한다.

provider.authenticate(authentication)에서 authenticate() 메소드의 매개변수로 authentication 객체를 전달하는 것을 확인할 수 있다.

하나의 프로젝트에 여러 Authentication Provider가 존재할 수 있기 때문에 ProviderManager(AuthenticationManager)의 역할이 모든 적용한 가능한 Authentication Provider의 authenticate() 메소드를 호출하여 인증을 시도하는 것임을 알 수 있다.

현재 AuthenticationProvider는 건들이지 않았으므로 default값이 호출될 것이고, default는 DaoAuthenticationProvider이다.


DaoAuthentcationProvider는 AbstractUserDetailsAuthenticationProvider를 extend하고 있고,


AbstractUserDetailsAuthenticationProvider는 AuthenticationProvider를 implement하는 것을 알 수 있다.


AuthenticationProvider에는 authenticate() 메소드가 있는 것 까지 확인할 수 있다.


AuthenticationProvider의 구현체를 보면, 총 16개의 구현체가 있음을 확인할 수 있고, 이 중 default provider로 DaoAuthenticationProvider가 호출된다.

이제 authenticate() 메소드에 중단점을 설정하고 내부 흐름을 분석해보겠다. DaoAuthenticationProvider의 authenticate() 메소드는 AbstractUserDetailsAuthenticationProvider에 있고, authenticate() 메소드의 첫 줄에 중단점을 설정하였다.

디버그를 해보면, authenticate() 메소드에서 UserDetail 정보가 cache에 있는지 확인하지만 cache에 정보가 없음을 알 수 있다.

이후 user가 null이므로, retreiveUser() 메소드를 호출한다.

retreiveUser()는 DaoAuthenticationProvider 안에 있고,

해당 메소드에서 UserDetailService의 loadUserByUsername() 메소드를 호출한다.

loadUserByUsername()를 디버그해보면 UserDetailsManager 인터페이스의 구현체 중 하나인 InMemoryUserDetailsManager로 이동한다.

해당 클래스는 UserDetailManager를 implement 하고 있음을 알 수 있고

이 클래스는 메모리 내부에 저장된 자격 증명을 확인하는 역할을 한다.
현재는 사용자의 자격 증명을 메모리에 저장하고 있으므로, InMemoryUserDetailManager를 사용하지만, 따로 DB에 사용자 자격을 저장하는 경우에는 JDBCUserDetailManager를 사용한다.

loadUserByUsername()에서는 username을 기반으로 UserDetail을 load하고, user가 null이 아니므로 username, password, isEnable, isAccountNonExpired 등의 정보를 사용하여 새로운 User 객체를 생성한다.

해당 메서드가 마무리 되면, 다시 AuthenticationProvider로 돌아간다.
지금까지는 AuthenticationProvider가 UserDetailManager, UserDetailService를 사용하여 UserDetail 정보를 로드했고, 이후에는 passwordEncoder를 사용하여 사용자 비밀번호가 올바른지를 확인한다. 이 작업은 addtionalAuthenticationChecks() 메서드에서 동작한다.

해당 메서드를 살펴보면,

presentedPassword라는 사용자가 입력한 password와 userDetails.getPassword()를 통해 메모리에서 가져온 password를 passwordEncoder를 통해 비교하여 올바른지를 확인하는 과정을 거친다.

비밀번호가 맞다면, DaoAuthenticationProvider에서 AbstractAuthenticationProcessingFilter로 이동하면서 브라우저 내부에서 성공적인 응답을 받는다.

브라우저에서 다시 새로고침을 하면, 이 때는 Security Context에 인증 정보가 저장되어있으므로 Provider, Manager와 같은 클래스들은 호출되지 않는다.

이 때는 AbstractAuthenticationProcessingFilter의 doFilter() 메서드에서 requiresAuthentication 값이 false로 출력되어 attemptAuthentication()와 같은 메서드들은 호출되지 않을 것이다.

지금까지는 Spring Security가 기본으로 제공해주는 인증 과정의 흐름을 파악해보았고, 차후 DB를 통한 자격 증명, 사용자 정의 Manager, Provider 등의 작동 방식도 알아보면 좋을 것 같다.

0개의 댓글