🎯 목표 : Spring Security 인증 처리 Flow 와 Component 에 대한 이해
📒 Spring Security Authentication Flow
📌 인증 처리 흐름
- Spring Security의 컴포넌트들이 어떤 과정을 거쳐 인증 요청을 처리하는지 정리해 보았다. 위 그림을 기반으로 정리했다.
- Username, Password 를 포함한 Request Data가 서버에 전송된다.
AbstractAuthenticationProcessingFilter
를 상속받은 UsernamePasswordAuthenticationFilter
에서 Username 과 Password를 이용하여 UsernamePasswordAuthenticationToken
을 생성한다. 이때, UsernamePasswordAuthenticationToken
은 Authentication
인터페이스를 구현한 클래스며, 아직 인증이 완료 되지 않은 Authentication
이다.
- 인증되지 않은
Authentication(= UsernamePasswordAuthenticationToken)
을 UsernamePasswordAuthenticationFilter
는 인증 처리를 총괄하는 인터페이스 AuthenticationManager
를 구현한 ProviderManager
에게 전달 한다.
ProviderManager
는 직접 인증처리를 하는 것이 아니라 인증 처리를 할 컴포넌트를 찾는데, 인증 처리를 직접적으로 하는 컴포넌트가 바로, AuthenticationProvider
다.
Authentication
를 전달받은 AuthenticationProvider
는 암호화로 저장되어 있는 Password 를 조회하기 위해 PasswordEncoder
를 통해 Authentication
의 Password 를 인코딩하여 다시 전달 받는다.
AuthenticationProvider
는 UserDetailsService
에 UserDetails
를 조회 한다. UserDetails
는 DB등 저장소에 저장된 사용자의 Username과 사용자의 자격을 증명해주는 크리덴셜인 Password 그리고 사용자의 권한 정보를 포함하고 있는 컴포넌트다.
UserDetailsService
는 저장소에서 사용자를 조회하여 조회한 사용자의 크리덴셜등 정보를 기반으로 UserDetails
를 생성한다.
UserDetailsService
는 생성한 UserDetails
을 AuthenticationProvider
에게 전달한다. 전달 받은 정보를 가지고 AuthenticationProvider
는 PasswordEncoder
를 이용하여 UserDetails
와 인증을 위한 Authentication
내의 Password(5 에서 전달받아놓은 인코딩 Password)가 일치하는지 검증한다.
- 검증에 성공하면 인증된
Authentication
을 생성한다.
- 실패하면 Exception 발생으로 인증 처리를 중단한다.
AuthenticationProvider
는 인증된 Authentication
을 ProviderManager
에게 전달한다.
ProviderManager
는 인증된 Authentication
을 UsernamePasswordAuthenticationFilter
에게 전달한다.
- 인증된
Authentication
을 UsernamePasswordAuthenticationFilter
는 SecurityContext
에 저장한다.
SecurityContext
는 세션 정책에 따라 세션에 저장되어 있는 사용자의 인증 상태를 유지하거나 유지하지 않는다.
📒 Spring Security Authentication Component
- 위 Flow 에서 언급한 Spring Security의 각 요소들을 정리해보자.
📌 UsernamePasswordAuthenticationFilter
- 사용자의 인증 요청을 가장 먼저 접하는 컴포넌트다. 일반적으로 폰에서 제출된 Username,Password를 통한 인증을 처리하는 필터다.
- Username, Password 를 Spring Security가 인증 프로세스에서 사용할수 있도록
UsernamePasswordAuthenticationToken
을 생성한다.
✅ UsernamePasswordAuthenticationFilter 코드 Click
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login","POST");
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
- 코드의 일부를 살펴보면
AbstractAuthenticationProcessingFilter
를 상속받는다. Filter에서 핵심 메소드인 doFilter()
는 상속받은 필터에 있다.
- 즉, 사용자의 인증 요청을 제일 먼저 전달 받는 Filter는
AbstractAuthenticationProcessingFilter
다.
- 클라이언트로 부터 전송되는 Request Parameter의 Default Name는
username
과 password
다.
AntPathRequestMatcher
는 클라이언트의 URL에 매치되는 객체다. "/login"
의 POST
메소드일 경우 매치되는 것을 알수 있다.
- 생성자로
AntPathRequestMatcher
와 AuthenticationManager
를 상위 클래스에 전달하는 것을 알수 있다.
attemptAuthentication()
는 전달 받은 username과 password 정보를 이용하여 인증을 시도하는 메소드다.
- Filter의 모든 작업의 시작점은
doFilter()
다. attemptAuthentication()
도 상위 클래스의 doFilter()
에 의해서 호출되는 것이다.
POST
메소드가 아니면 Exception을 던진다.
- username과 password를 이용하여
UsernamePasswordAuthenticationToken
을 생성하는데 인증 프로세스를 진행하기 위한 토큰이지 인증에 성공한 인증 토큰과 상관 없는 토큰이다.
- 마지막으로,
this.getAuthenticationManager()
로 AuthenticationManager
의 .authenticate(authRequest);
메소드를 호출하여 인증 처리를 위임 한다.
📌 AbstractAuthenticationProcessingFilter
UsernamePasswordAuthenticationFilter
가 상속받는 클래스 AbstractAuthenticationProcessingFilter
를 알아보자.
AbstractAuthenticationProcessingFilter
는 HTTP 기반의 인증 요청을 처리하지만 실제 인증 시도는 하위 클래스에 맡긴다.
- 인증에 성공하게 되면 인증된 사용자의 정보를
SecurityContext
에 저장하는 역할을 한다.
✅ AbstractAuthenticationProcessingFilter 코드 Click
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
unsuccessfulAuthentication(request, response, ex);
}
}
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
if (this.requiresAuthenticationRequestMatcher.matches(request)) {
return true;
}
if (this.logger.isTraceEnabled()) {
this.logger
.trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher));
}
return false;
}
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
this.securityContextRepository.saveContext(context, request, response);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
this.logger.trace("Failed to process authentication request", failed);
this.logger.trace("Cleared SecurityContextHolder");
this.logger.trace("Handling authentication failure");
this.rememberMeServices.loginFail(request, response);
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
}
- 코드의 일부를 살펴보면,
doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
를 통하여 Spring Security의 필터라는 것을 알수 있다.
- 내부를 살펴보면,
AbstractAuthenticationProcessingFilter
클래스가 인증 처리를 해야되는지 아니면 다음 필터를 호출할지 여부를 결정하고 있다.
requiresAuthentication()
를 호출하여 하위 클래스에서 전달받은 requiresAuthenticationRequestMatcher
객체를 통해 요청이 인증 처리를 해야하는지 여부를 결정하고 있다.
- try 절에서는
UsernamePasswordAuthenticationFilter
에 인증을 시도해 줄 것을 요청하고 있다.
- try 절 마지막 부분에서는
successfulAuthentication()
를 호출하여 인증에 성공 했을때 처리할 동작을 수행한다. successfulAuthentication()
는 인증 정보를 SecurityContext
에 저장한뒤 HttpSession
에 저장한다.
- 인증에 실패한다면,
unsuccessfulAuthentication()
를 호출하여 SeucurityContext
를 초기화 하고 AuthenticationFailureHandler
를 호출한다.
📌 UsernamePasswordAuthenticationToken
- Spring Security에서 인증을 수행하기 위해 필요한 토큰이며 인증 성공 후 인증에 성공한 사용자의 인증 정보가 토큰에 포함되어
Authentication
객체 형태로 SecurityContext
에 저장된다.
✅ UsernamePasswordAuthenticationToken 코드 Click
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
private Object credentials;
public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
return new UsernamePasswordAuthenticationToken(principal, credentials);
}
public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
return new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
}
}
- 코드의 일부를 살펴보면, principal과 credentials 필드를 가지고 있다.
- principal은 Username 등 신원을 의미한다.
- credentials는 Password를 의미한다.
unauthenticated()
에서 인증에 필요한 용도의 UsernamePasswordAuthenticationToken
객체를 생성한다.
authenticated()
는 인증에 성공 한 후 SecurityContext
에 저장될 UsernamePasswordAuthenticationToken
를 생성한다.
📌 Authentication
- 인증 자체를 표현하는 인터페이스다.
UsernamePasswordAuthenticationToken
은 AbstractAuthenticationToken
추상 클래스를 상속하는 클래스이며, Authentication
인터페이스 일부를 구현하는 클래스 이다.
UsernamePasswordAuthenticationToken
타입의 인증을 위해 생성되는 인증 토큰과 인증 성공 후 생성되는 토큰이 SecurityContext
에 저장될 경우 Authentication
형태로 리턴 받거나 저장된다.
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
- Principal은 사용자를 식별하는 고유 정보다. 일반적으로 Username/Password 기반 인증에서 Username이 Principal이 된다. 다른 인증 방식에서는 UserDetails가 Princiapl이 된다.
- Credentials는 사용자 인증에 필요한 Password를 의미한다. 인증이 이루어지고 난 후
ProviderManager
는 해당 Credentials를 삭제한다.
- Authorities는 사용자 접근 권한 목록이다.
GrantedAuthority
의 구현 클래스는 SimpleGrantedAuthority
이다.
📌 AuthenticationManager
- 인증 처리를 총괄하는 매니저 역할의 인터페이스다.
authenticate()
메소드 하나만 정의되어 있는데, 인증을 위한 필터들은 AuthenticationManager
를 통해 느슨한 결합을 유지하고 있고 실질적인 관리는 AuthenticationManager
를 구현한 클래스를 통하여 관리된다.
📌 ProviderManager
- Spring Security 에서
AuthenticationManager
를 구현한 클래스는 일반적으로 ProviderManager
를 말한다.
✅ ProviderManager 코드 Click
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {
Assert.notNull(providers, "providers list cannot be null");
this.providers = providers;
this.parent = parent;
checkState();
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
((CredentialsContainer) result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
}
}
ProviderManager
클래스가 Bean 등록시 List<AuthenticationProvider>
의존성 주입을 받는것을 알수 있다.
authenticate()
에서는 for 문으로 적절한 AuthenticationProvider
를 찾아 인증처리를 위임한다.
- 인증이 처리되면
.eraseCredentials()
를 호출하여 사용된 Credentials를 제거한다.
📌 AuthenticationProvider
AuthenticationManager
로부터 인증 처리를 위임 받아 실질적인 인증 수행을 담당한다.
- Username/Password 기반의 인증 처리는
DaoAuthenticationProvider
가 담당 하고 있고, UserDetailsService
로 전달 받은 UserDetails
를 이용하여 인증을 처리한다.
✅ AuthenticationProvider 코드 Click
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private PasswordEncoder passwordEncoder;
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
DaoAuthenticationProvider
는 AbstractUserDetailsAuthenticationProvider
를 상속 받는다.
AuthenticationProvider
의 구현한 추상 클래스가 AbstractUserDetailsAuthenticationProvider
이고, 그것을 확장한 클래스가 DaoAuthenticationProvider
다.
- 즉, 추상 클래스의
authenticate()
메소드로 부터 실제 인증 처리가 시작된다.
retrieveUser()
는 UserDetailsService
로부터 UserDetails
를 조회하는 역할을 한다. 이는 사용자를 인증하는데 사용되며, 인증에 성공할 경우 인증된 Authentication
객체를 생성하는데 사용된다.
additionalAuthenticationChecks()
에서는 PasswordEncoder
를 이용하여 사용자의 패스워드를 검증하고 있다.
- 확장 클래스와 구현 추상 클래스의 메소드 호출 순서 요약
AbstractUserDetailsAuthenticationProvider
authenticated()
메서드 호출
DaoAuthenticationProvider
retrieveUser()
메서드 호출
DaoAuthenticationProvider
additionalAuthenticationChecks()
메서드 호출
DaoAuthenticationProvider
createSuccessAuthentication()
메서드 호출
AbstractUserDetailsAuthenticationProvider
createSuccessAuthentication()
메서드 호출
- 인증된
Authentication
을 ProviderManager
에게 리턴
📌 UserDetails
- 데이터베이스 등 저장소에 저장된 사용자의 Username과 사용자의 자격을 증명해주는 크리덴셜, 사용자의 권한 정보를 포함하는 컴포넌트다.
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
- 사용자의 권한 정보, 패스워드, 유저네임을 포함하고 있고 계정에 대한 만료 여부, Lock 여부, 크리덴셜의 만료여부, 활성화 여부에 대한 정보를 포함하고 있다.
📌 UserDetailsService
UserDetails
를 로드하는 핵심 인터페이스다.
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
loadUserByUsername(String username)
를 통해 사용자의 정보를 로드한다.
📌 SecurityContext & SecurityContextHolder
SecurityContextHolder
가 SecurityContext
를 포함하고 있고 SecurityContext
는 Authentication
를 포함하고 있다.
- 즉,
SecurityContextHolder
를 통해 인증된 Authentication
를 SecurityContext
에 설정할 수 있고 객체에 접근할 수 있다는 뜻이다.
SecurityContextHolder
의 기본 전략은 ThreadLocalSecurityContextHolderStrategy
다.
- ThreadLocal은 쓰레드 간에 공유되지 않는 쓰레드 고유의 로컬 영역을 말한다.
- WebMVC 기반 프로젝트는 일반적인 경우 요청 하나에 쓰레드 하나를 생성한다.
- 쓰레드 마다 고유한 공간을 만들수 있고 그곳에
SecurityContext
를 저장한다.
getContext()
메소드로 현재 실행 쓰레드의 SecurityContext
를 얻을 수 있다.
setContext()
메소드는 현재 쓰레드에 SecurityContext
를 연결한다. 일반적으로 인증된 Authentication
을 포함한 SecurityContext
를 연결하는데 사용한다.