전편에 Security 모듈이 어떤 식으로 구성되어 있고, Spring Framework와는 어떻게 연결되어 있는지 알아봤다.
이번에는 이 녀석이 하는 일, 인증 그 자체에 대해 알아보자.
우선 Authentication의 아키텍쳐 구성 요소들은 다음과 같다.
SecurityContextHolder
에 담겨있으며, 현재 인증된 사용자에 대한 Authentication
을 담고있다.SecurityContext
내부에 있으며, 현재 사용자 아이디나 인증키 (e.g. 비밀번호)를 AuthenticationManager
에 제공하는 입력값으로 쓰일 수 있다.AuthenticationManager
의 가장 보편적인 구현체.ProviderManager
에 의해 사용되는 객체.부가 설명이 필요할 것 같은 몇 개만 알아보자.
Spring Security의 인증 모델의 심장이라 할 만한 게 바로 이 부분이다.
인증된 사용자에 대한 상세정보가 저장되는 공간이며,
Spring Security는 이 공간을 채우고, 참조도 하되 어떻게 채워지는지는 신경을 쓰지 않는다.
다시 말해, 어쨌거나 이 공간에 값이 존재하고 있다면, 그건 현재 인증이 된 유저로서 사용할 수 있다는 이야기.
제한된 경로의 api 테스트를 할 때 유용한 개념이다.
가장 간단한 방법은 직접적으로 SecurityContextHolder
를 세팅하는 것이다.
예시는 아래와 같다.
// 1
SecurityContext context = SecurityContextHolder.createEmptyContext();
// 2
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);
// 3
SecurityContextHolder.setContext(context);
SecurityContext
를 생성한다.Authentication
객체를 생성해 SecurityContext
에 넣는다. Spring Security는 어떤 타입의 Authentication
구현체가 SecurityContext
에 자리를 잡는지 신경쓰지 않는다. TestingAuthenticationToken
은 간단해서 테스트용으로 사용하기가 좋고, 가장 보편적인 케이스는 UsernamePasswordAuthenticationToken(userDetails, password, authorities)
를 사용하는 것이다.SecurityContext
를 SecurityContextHolder
에 넣는다. Spring Security는 authorization
과정을 위해 이 정보를 이용하게 된다.SecurityContextHolder.getContext().setAuthentication(authentication)
과 같이 한번에 끝내지 않고, step by step으로 빈 객체를 생성하고 채워가는 이유는 다중 스레드 환경에서 스레드 간의 race condition 발생을 피하기 위함이다.인증된 사용자 정보에 접근하는 법은 다음과 같다.
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities =
authentication.getAuthorities();
SecurityContextHolder
는 기본적으로 정보를 저장할 때 ThreadLocal
을 사용한다. 다시 말해, SecurityContext
은 동일 스레드 상의 메소드에서도 언제나 접근 가능하다는 말이다.
애플리케이션의 속성이나 각종 경우에 따라 ThreadLocal
을 사용하는 것이 어울리지 않을 수도 있다.
이럴 때는 2가지 옵션이 있다.
SecurityContextHolder.MODE_GLOBAL
: 모든 스레드에서 하나의 SecurityContextHolder
를 사용하는 방식이다.SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
: 보안된 스레드에서 생성된 스레드 또한 동일한 보안 identity를 가져가는 방식이다.그리고 기본값으로
SecurityContextHolder.MODE_THREADLOCAL
가 있다.Spring Security 내에서 크게 두 가지 일을 한다.
AuthenticationManager
에 credentials을 제공한다.Authentication의 요소는 세 가지이다.
UserDetails
의 객체이다.사용자에게 부여된 높은 수준의 권한이다.
Authentication.getAuthorities()
를 통해 확인이 가능하며, Collection형의 객체를 반환한다.
명시된 권한이나 역할들은 웹 권한이나 메소드 권한 등에 이용된다.
username/password 타입의 Authentication이라면 UserDetailsService
에 의해 로드된다.
AuthenticationManager
의 가장 보편적인 구현체이다.
위 사진과 같이 ProviderManager
는 AuthenticationProvider
체인에 일을 맡긴다.
이때, 각 AuthenticationProvider
는 접수 된 인증이 성공인지, 실패인지, 혹은 알 수가 없어서 다음 AuthenticationProvider
에게 일을 넘겨야 하는 지 결정한다.
만약 인증에 성공한 AuthenticationProvider
가 하나도 없으면, 인증은 자연스레 실패로 처리되고, AuthenticationException
의 일종인 ProviderNotFoundException
을 던진다.
인증에 성공한 경우가 있다면, 반환되는 Authentication
객체에서 비밀번호 같은 인증키를 지운다. 왜? 유출을 막기 위해서.
쉽게 말해 이런 거다.
요즘은 어떤 사이트에 들어가면, 보통 아이디/비밀번호를 치고 들어가거나 소셜 로그인을 통해 들어가거나 선택을 한다.
이 때 아이디/비밀번호의 인증을 담당하는 AuthenticationProvider
가 있고, 소셜 로그인을 담당하는 AuthenticationProvider
가 있는 거다.
사용자가 어느쪽을 선택하던, ProviderManager
에 의해 AuthenticationProvider
는 주어진 정보로 한 번씩 인증을 시도해보는 거고, 뭐라도 되면 성공, 전부 안되면 실패다.
바로 위에서 적당히 설명했지만, 특정 타입의 인증 방식을 지원하는 Provider이다.
username/password 기반으로 인증을 하는 DaoAuthenticationProvider,
JWT token으로 인증하는 JwtAuthenticationProvider,
소셜 로그인으로 인증을 대신하는 CommonOAuth2Provider 등이 있다.
사용자의 인증에 사용되는 필터들의 base class이다.
실질적인 객체가 없는 추상 클래스이지만, 기본적인 흐름을 알아보기 위해 가져왔다.
자격이 인증되기 전에 Spring Security는 일반적으로 AuthenticationEntryPoint
를 통해 자격 증명을 요청한다. 그 후에, AbstractAuthenticationProcessingFilter
에 의해 인증 요청들이 수행될 수 있다.
대략적인 과정은 아래와 같다.
AbstractAuthenticationProcessingFilter
가 HttpServletRequest
로부터 정보를 뽑아 Authentication
을 생성한다. 이때, Authentication
의 타입은 실제로 이용되고 있는 AbstractAuthenticationProcessingFilter
자식클래스에 따른다.UsernamePasswordAuthenticationFilter
는 UsernamePasswordAuthenticationToken
을 생성한다.Authentication
이 AuthenticationManager
에 전달된다.SecurityContextHolder
가 비워진다.RememberMeServices.loginFail
가 호출된다. 만약 RememberMeServices
가 구성되지 않았을 경우, 이 작업은 건너뛴다.AuthenticationFailureHandler
가 호출된다.SessionAuthenticationStrategy
가 새 로그인에 대한 알림을 받는다.SecurityContextHolder
에 Authentication
이 담긴다. 그 후에 SecurityContextPersistenceFilter
에 의해 SecurityContext
가 HttpSession
에 저장된다. (해당 필터에 대해서는 이전 글을 보자).RememberMeServices.loginSuccess
가 호출된다. 만약 RememberMeServices
가 구성되지 않았을 경우, 이 작업은 건너뛴다.ApplicationEventPublisher
에 의해 InteractiveAuthenticationSuccessEvent
가 퍼블리싱 된다.AuthenticationSuccessHandler
가 호출된다.다음 편에서는 Authentication의 주요 매커니즘에 대해 알아보자.
원래 여기에 합쳐서 하려고 했는데, 다음 내용이 생각보다 길 것 같아 나누기로 했다.