이번 포스트에서는 Servlet Application에서 Spring Security의 Authentication Architecture에 대해 살펴보자.
Authentication Architecture
SecurityContextHolder
SecurityContextHolder는인증된 사용자의 정보를 저장하는 보관소이다. 사용자 인증 정보는 SecurityContext가 갖는다.
SecurityContextHolder는 ThreadLocal을 이용해서 사용자 인증 정보를 저장한다. 따라서 동일한 요청 스레드 내의 메서드에서 SecurityContext에 자유롭게 접근할 수 있으며 SecurityContext를 메서드의 인자로 명시적으로 전달할 필요가 없다.
ThreadLocal은 각 스레드에 대해 독립적인 변수를 저장할 수 있게 해주는 객체로 ThreadLocal에 저장된 값은 해당 변수를 설정한 스레드에서만 접근할 수 있고 다른 스레드에서는 그 값을 볼 수 없다.이를 통해 사용자 인증 정보, 세션 데이터 등을 스레드 별로 안전하게 처리할 수 있다.
SecurityContext
SecurityContext는 현재 인증된 사용자의 Authentication 객체를 갖는다. Authentication은 다시Principal(사용자 식별 정보), Credentials(비밀번호), Authorities(권한)를 갖는다.
Authentication
Authentication 객체는 Spring Security에서 시점에 따라 크게 2가지로 사용된다.
인증 전: 사용자가 인증을 위해 AuthenticationManager에게 제공한 자격증명 정보(id/pass 등)이다. 이때는 아직 인증이 완료되지는 않았으므로 isAuthenticated()는 false를 반환한다.
인증 후: 현재 인증된 사용자의 정보를 보여준다. 현재 인증된 사용자의 Authentication은 SecurityContext에서 얻을 수 있다.
Authentication은 Principal, Credentials, Authorities를 갖는다.
Principal: 사용자를 식별하는 정보로 username/password를 통해서 인증을 처리할 때 UserDetails 타입의 객체를 사용한다.
Credentials: Password에 해당하며 일단 인증이 완료되면 보안을 위해 이 정보는 삭제하는 경우가 많다.
Authorities: GrantedAuthority 인스턴스로 ROLE_ADMIN, ROLE_USER 등 사용자가 부여받은 권한을 의미한다. 이 객체는 UserDetailsService에 의해 로딩된다.
인증관리
AuthenticationManager
AuthenticationManager는인증을 수행하는 방법을 정의한 인터페이스로 사용자의 인증 정보를 받아서 검증하는 역할을 수행한다. AuthenticationManager에서 반환된 인증은 호출한 컨트롤러(Spring Security의 필터)에 의해 SecurityContextHolder에 저장된다.AuthenticationManager의 가장 일반적인 구현은 ProviderManager이다.
ProviderManager
ProviderManager는실제 인증 작업을 수행하는 객체이다. ProviderManager는 여러 AuthenticationProvider 인스턴스들에게 인증 처리를 위임한다. AuthenticationProvider는 순차적으로 [인증성공], [실패], [결정할 수 없음]을 판단한 후 다음 AuthenticationProvider에게 전달하고 어떤 Provider도 인증을 성공하지 못한다면 ProviderNotFoundException이 발생한다.
AuthenticationProvider
AuthenticationProvider는 실제로 특정 유형의 인증을 처리한다. 예를 들어 DaoAuthenticationProvider는 DB에서 비밀번호를 검증하고, JwtAuthenticationprovider는 JWT 토큰 인증을 지원한다.
인증 관련 처리
AuthenticationEntryPoint
인증이 필요한 리소스에 인증되지 않은 사용자가 접근할 때 사용자에게 로그인을 요청하는 역할을 수행한다. 예를 들어로그인 페이지로 redirect 하거나 login이 필요하다는 정보를 응답을 통해 보낼 수도있다.
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
// API 요청인 경우 401 응답
if (request.getHeader("Accept").contains("application/json")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"error\":\"인증이 필요합니다\"}");
} else {
// 웹 페이지 요청인 경우 로그인 페이지로 리다이렉트
response.sendRedirect("/login");
}
}
}
// SecurityConfig에 등록
http.exceptionHandling(handling -> handling
.authenticationEntryPoint(authenticationEntryPoint)
);
AuthenticationFailureHandler
AuthenticationFailureHandler는 사용자가 로그인을 시도했으나 잘못된 비밀번호, 계정 잠금등의 이유로 인증에 실패했을 때 동작한다.
@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String errorMessage = "";
if (exception instanceof BadCredentialsException) {
errorMessage = "아이디나 비밀번호가 맞지 않습니다";
} else if (exception instanceof DisabledException) {
errorMessage = "계정이 비활성화되었습니다";
}
// 로그인 페이지로 리다이렉트
response.sendRedirect("/login?error=" + URLEncoder.encode(errorMessage, "UTF-8"));
}
}
// HttpSecurity에 설정
http.formLogin(form -> form
.failureHandler(authenticationFailureHandler)
);
인증 흐름 관리
AbstractAuthenticationProcessiogFilter
인증 처리를 위한 기본 필터로 전체적인 인증 흐름을 관리한다.
사용자가 credentials(비밀번호)를 제출하면 AbstractAuthenticationProcessingFilter가 Authentication 객체를 생성한다.
Authentication 객체는 AuthenticationManager에 전달되서 인증 절차를 밟는다. AuthenticationManager는 ProviderManager에게 인증을 요청하고 ProviderManager는 관리하는 AuthenticationProviderManager들을 통해 인증한다.
만약 Authentication에 실패하면 SecurityContextHolder를 깨끗히 비우고 RememberMeService의 loginFail이 호출된다. 마지막으로 AuthenticationFailureHandler가 호출된다.
Authentication에 성공하면 SessionAuthenticationStrategy에 새로운 로그인에 대한 알람을 받는다. Authentication 객체가 SecurityContextHolder에 저장된다. RememberMeService를 설정했다면 RememberMeService#loginSuccess가 호출된다. ApplicationEventPublisher가 InteractiveAuthenticationSuccessEvent를 발행하고 마지막으로 AuthenticationSuccessHandler가 호출된다.