안전하게 보호된 애플리케이션을 개발하기 위해 반드시 익혀야 되는 중요한 보안 요소는 두 가지인데, 그 중 하나는 바로 인증(Authentication)이다.
Spring Security에서는 Spring Security Filter Chain을 통해 보안을 위한 특정 작업을 처리한다는 것을 알게 되었다. 그럼, Spring Security Filter Chain에 사용자의 인증 요청이 전달되었다면 이후의 처리는 어떻게 될까?
사용자가 로그인 인증을 위한 요청을 전송할 경우, Spring Security에서 해당 인증 요청을 어떻게 처리하는지를 한 눈에 볼 수 있는 Spring Security 핵심 컴포너트로 구성된 인증 처리 흐름이다.
가장 일반적으로 사용하는 인증 방식이 ID, Password(Spring Security의 Username, Password)를 이용한 로그인 인증 방식이기 때문에, 로그인 인증에 대한 처리 흐름으로 구성했다.
(1)에서 사용자가 로그인 폼 등을 이용해 Username(ID)과 Password를 포함한 request를 Spring Security가 적용된 애플리케이션에 전송한다.
사용자의 로그인 요청이 Spring Security의 Fiter Chain까지 들어오면, 여러 Filter들 중에서 UsernamePasswordAuthenticationFilter
가 해당 요청을 전달 받는다.
사용자의 로그인 요청을 전달 받은 UsernamePasswordAuthenticationFilter
는 Username과 Password를 이용하여 (2)와 같이 UsernamePasswordAuthenticationToken
을 생성한다.
UsernamePasswordAuthenticationToken
은 Authentication
인터페이스를 구현한 클래스이다. Authentication
은 아직 인증되지 않았다.
인증되지 않은 Authentication
을 갖고 있는 UsernamePasswordAuthenticationFilter
는 (3)과 같이 해당 Authentication
을 AuthenticationManager
에게 전달한다.
AuthenticationManager
는 인증 처리를 총괄하는 인터페이스로, 매니저 역할을 한다. AuthenticationManager
룰 구현한 인터페이스는 ProviderManager
이다. 즉, ProviderManager
가 인증 작업을 총괄하는 실질적인 매니저이다.
💡 현실에서의 매니저 혹은 지배인의 역할을 떠올려보면 이해가 쉬울 것이다.
레스토랑의 매니저는 손님에게 직접 주문을 받거나, 음식을 서빙하는 등의 일을 하지 않는다. 매니저는 레스토랑에서 일어나는 모든 일을 총괄하고 관리하는 역할을 한다.
Spring Security의ProviderManager
역시, 직접 인증을 처리하지 않고 인증을 처리할 누군가를 찾아 인증 처리를 맡긴다. 인증을 처리할 누군가는AuthenticationProvider
이다.
(4)와 같이 ProviderManager
으로부터 Authentication
을 전달 받은 AuthenticationProvider
는 UserDetailsService
를 이용해 UserDetails
를 조회한다.
UserDetails
는 DB 등 저장소에 저장된 사용자의 Username과 사용자 자격을 증명해주는 크리덴셜(Credential)인 Password, 그리고 사용자의 권한 정보를 포함하고 있는 컴포넌트이다.
이 UserDetails
를 제공하는 컴포넌트가 바로 UserDetailsService
이다.
UserDetailsService
는 (5)에서 처럼 DB 등 저장소에서 사용자의 크리덴셜(Credential)을 포함한 사용자의 정보를 조회한다. (6)
저장소에서 조회한 사용자의 크리덴셜을 포함한 사용자 정보를 기반으로 (7)과 같이 UserDetails
를 생성한 후, 생성된 UserDetails
를 다시 AuthenticationProvider
에게 전달한다. (8)
UserDetails
를 전달 받은 AuthenticationProvider
는 PasswordEncoder
를 이용해 UserDetails
에 포함된 암호화 된 Password
와 인증을 위한 Authentication
안에 포함된 Password
가 일치하는지 검증한다.
검증에 성공하면 UserDetails
를 이용해 인증된 Authentication
을 생성한다. (9)
검증에 실패하면 Exception을 발생시키고 인증 처리를 중단한다.
AuthenticationProvider
는 인증된 Authentication
을 ProviderManager
에게 전달한다. (10)
(2)에서의 Authentication
은 인증을 위해 필요한 사용자의 로그인 정보를 가지고 있지만, 이 단계에서 ProviderManager
에게 전달한 Authentication은 인증에 성공한 사용자의 정보(Principal, Credential, GrantedAuthorities)를 가지고 있다.
이제 ProviderManager
는 (11)과 같이 인증된 Authentication
을 다시 UsernamePasswordAuthenticationFilter
에게 전달한다.
인증된 Authentication
을 전달 받은 UsernamePasswordAuthenticationFilter
는 마지막으로 (12)와 같이 SecurityContextHolder
를 이용해 SecurityContext
에 인증된 Authentication
을 저장한다.
SecurityContext
는 Spring Security의 세션 정책에 따라서 HttpSession
에 저장되어 사용자의 인증 상태를 유지하기도 하고, HttpSession을 생성하지 않고 무상태를 유지하기도 한다.
사용자의 로그인 요청을 처리하는 Spring Security Filter는 UsernamePasswordAuthenticationFilter
이다.
UsernamePasswordAuthenticationToken
은 Authentication
인터페이스를 구현한 구현 클래스이며, 여기서의 Authentication
은 아직 인증이 되지 않은 Authentication을 의미한다.
AuthenticationManager
는 인증 처리를 총괄하는 매니저 역할을 하는 인터페이스이고, AuthenticationManager
를 구현한 구현 클래스가 ProviderManager
이다.
UserDetails는 데이터베이스 등의 저장소에 저장된 사용자의 Username과 사용자의 자격을 증명해주는 크리덴셜(Credential)인 Password, 그리고 사용자의 권한 정보를 포함하고 있는 컴포넌트이다.
UserDetails
를 제공하는 컴포넌트가 바로 UserDetailsService
이다.
UserDetailsService
는 데이터베이스 등의 저장소에서 사용자의 크리덴셜(Credential)을 포함한 사용자의 정보를 조회하여 AuthenticationProvider
에게 제공한다.
UsernamePasswordAuthenticationFilter
가 생성하는 Authentication
은 인증을 위해 필요한 사용자의 로그인 정보를 가지고 있지만, AuthenticationProvider
가 생성한 Authentication
은 인증에 성공한 사용자의 정보(Principal, Credential, GrantedAuthorities)를 가지고 있다.
인증된 Authentication
을 전달 받은 UsernamePasswordAuthenticationFilter
는 SecurityContextHolder
를 이용해 SecurityContext
에 인증된 Authentication
을 저장한다. SecurityContext
는 다시 HttpSession
에 저장되어 사용자의 인증 상태를 유지한다.
Spring Security의 인증 처리 흐름 그림에서 로그인 request를 제일 먼저 만나는 컴포넌트는 Spring Security Filter Chain의 UsernamePasswordAuthenticationFilter
이다.
UsernamePasswordAuthenticationFilter
는 일반적으로 로그인 폼에서 제출되는 Username과 Password를 통한 인증을 처리하는 Filter이다.
클라이언트로부터 전달 받은 Username과 Password를 Spring Security가 인증 프로세스에서 이용할 수 있게 UsernamePasswordAuthenticationToken
을 생성한다.
✅ UsernamePasswordAuthenticationFilter
클래스의 역할
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { // (1)
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username"; // (2)
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password"; // (3)
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login","POST"); // (4)
...
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager); // (5)
}
// (6)
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
// (6-1)
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
...
// (6-2)
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
...
return this.getAuthenticationManager().authenticate(authRequest); // (6-3)
}
...
}
UsernamePasswordAuthenticationFilter
는 (1)과 같이 AbstractAuthenticationProcessingFilter
를 상속한다.
UsernamePasswordAuthenticationFilter 클래스에는 doFilter()
메서드가 존재하지 않는다.
Filter가 Filter의 역할을 하기 위해서는 doFilter()
메서드가 있어야 하는데, doFilter()
메서드는 어디에 있을까?
상위 클래스 AbstractAuthenticationProcessingFilter가 doFilter()
메서드를 포함하고 있다.
사용자의 로그인 request를 제일 먼저 전달 받는 클래스는 UsernamePasswordAuthenticationFilter의 상위 클래스인 AbstractAuthenticationProcessingFilter 클래스인 것이다.
(2)와 (3)을 통해 클라이언트의 로그인 폼을 통해 전송되는 request parameter의 디폴트 name은 username
과 password
인 것을 알 수 있다.
(4)의 AntPathRequestMatcher
는 클라이언트의 URL에 매치되는 매처이다.
클라이언트의 URL이 /login
이고 HTTP Methid가 POST
인 경우, 매치될 것이라고 예상할 수 있다.
ᅠ
(4)에서 생성되는 AntPathRequestMatcher의 객체(DEFAULT_ANT_PATH_REQUEST_MATCHER
)는 (5)에서 상위 클래스인 AbstractAuthenticationProcessingFilter 클래스에 전달되어 Filter가 구체적인 작업을 수행할지 특별한 작업 없이 다른 Filter를 호출할지 결정하는데 사용된다.
(5)에서 AntPathRequestMatcher의 객체(DEFAULT_ANT_PATH_REQUEST_MATCHER
)와 AuthenticationManager
를 상위 클래스인 AbstractAuthenticationProcessingFilter에 전달한다.
(6)의 attemptAuthentication()
메서드는 클라이언트에서 전달한 username과 password 정보를 이용해 인증을 시도한다.
⭐ attemptAuthentication()
메서드는 상위 클래스인 AbstractAuthenticationProcessingFilter의 doFilter()
메서드에서 호출되는데, Filter에서 어떤 처리를 하는 시작점은 doFilter()
이다.
(6-1)은 HTTP Method가 POST
가 아니면 Exception을 throw 한다.
(6-2)에서는 클라이언트에서 전달한 username과 password 정보를 이용해 UsernamePasswordAuthenticationToken
을 생성한다.
⭐ 여기서의 UsernamePasswordAuthenticationToken
은 인증을 하기 위해 필요한 인증 토큰이다. 인증에 성공한 인증 토큰과는 상관이 없다.
(6-3)에서 AuthenticationManager의 authenticate()
메서드를 호출해 인증 처리를 위임하는 것을 볼 수 있다.
AbstractAuthenticationProcessingFilter
클래스는 UsernamePasswordAuthenticationFilter
가 상속하는 상위 클래스로써 Spring Security에서 제공하는 Filter 중 하나이다.
✅ AbstractAuthenticationProcessingFilter
클래스의 역할
⭐ AbstractAuthenticationProcessingFilter는 HTTP 기반의 인증 요청을 처리하지만, 실질적인 인증 시도는 하위 클래스에 맡기고 인증에 성공하면 인증된 사용자의 정보를 SecurityContext에 저장한다.
// 코드 생략
(1)을 통해 AbstractAuthenticationProcessingFilter
클래스가 Spring Security의 Filter임을 알 수 있다.
(1-1)에서는 AbstractAuthenticationProcessingFilter 클래스가 인증 처리를 해야되는지, 아니면 다음 Filter를 호출할지 여부를 결정하한다.
(1-1)에서 호출하는 requiresAuthentication()
메서드는 하위 클래스에서 전달받은 requiresAuthenticationRequestMatcher 객체를 통해 들어오는 요청이 인증 처리를 해야 되는지 여부를 결정한다.
AntPathRequestMatcher("/login","POST")
의 파라미터인 URL과 HTTP Method가 매칭 조건이 된다.
(1-2)에서는 하위 클래스에 인증을 시도해 줄 것을 요청한다. 여기서의 하위 클래스는 UsernamePasswordAuthenticationFilter
가 된다.
(1-3)에서는 인증에 성공하면 처리할 동작을 수행하기 위해 successfulAuthentication()
메서드를 호출한다.
successfulAuthentication()
메서드는 (3)에서 확인할 수 있는데, 인증에 성공한 이후 ⭐ SecurityContextHolder를 통해 사용자의 인증 정보를 SecurityContext에 저장한 뒤, SecurityContext를 HttpSession에 저장한다.
만약 인증에 실패하면, (1-4)와 같이 unsuccessfulAuthentication()
메서드를 호출하여 인증 실패 시 처리할 동작을 수행한다.
unsuccessfulAuthentication()
메서드는 (4)에서 확인할 수 있다시피 SeucurityContext를 초기화 하고, AuthenticationFailureHandler를 호출합니다.
UsernamePasswordAuthenticationFilter
는 클라이언트로부터 전달 받은 Username과 Password를 Spring Security가 인증 프로세스에서 이용할 수 있도록 UsernamePasswordAuthenticationToken
을 생성한다.
AbstractAuthenticationProcessingFilter
는 HTTP 기반의 인증 요청을 처리하지만, 실질적인 인증 시도는 하위 클래스에 맡기고, 인증에 성공하면 인증된 사용자의 정보를 SecurityContext에 저장하는 역할을 한다.
Authentication
은 Spring Security에서의 인증 자체를 표현하는 인터페이스이다.
AuthenticationManager
는 이름 그대로 인증 처리를 총괄하는 매니저 역할을 하는 인터페이스이며, 인증을 위한 실질적인 관리는 AuthenticationManager를 구현하는 구현 클래스를 통해 이루어진다.
ProviderManager
는 이름에서 유추할 수 있듯이 AuthenticationProvider를 관리하고, AuthenticationProvider에게 인증 처리를 위임하는 역할을 한다.
AuthenticationProvider
는 AuthenticationManager로부터 인증 처리를 위임 받아 실질적인 인증 수행을 담당하는 컴포넌트이다.
UserDetails
는 데이터베이스 등의 저장소에 저장된 사용자의 Username과 사용자의 자격을 증명해주는 크리덴셜(Credential)인 Password, 그리고 사용자의 권한 정보를 포함하는 컴포넌트이며, AuthenticationProvider
는 UserDetails
를 이용해 자격 증명을 수행한다.
UserDetailsService
는 UserDetails를 로드(load)하는 핵심 인터페이스이다.
SecurityContext
는 인증된 Authentication 객체를 저장하는 컴포넌트이고, SecurityContextHolder
는 SecurityContext를 관리하는 역할을 담당한다.