Spring Security 내부 구현체 알아보기 두 번째 포스팅으로는 인증 요청을 가로채는 AuthenticationFilter
와 AuthenticationProvider를 관리하는 AuthenticationManager
, 그리고 실제 인증 로직을 처리하는 AuthenticationProvider
를 살펴보고자 한다.
첫 번째 포스팅에서는 인증 과정의 End-Point를 중점으로 봤다면, 이후 포스팅에서는 HTTP 요청을 받은 AuthenticationFilter 시점부터 전반적인 흐름을 순차적으로 살펴보고자한다.
우리가 관심있는 부분은 UsernamePasswordAuthenticationFilter
의 핵심 로직이 내포된 attemptAuthentication() 메서드이다.
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);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
위 메서드를 살펴보면 AuthenticationFilter
의 역할은 다음과 같다.
그럼 여기서 궁금할 점은 이 UsernamePasswordAuthenticationFilter
의 attemptAuthentication() 메서드는 어디서 실행된 것인지이다.
바로 인증 처리 기능을 하고있는 AbstractAuthenticationProcessingFilter의 doFilter()메서드이다.
UsernamePasswordAuthenticationFilter
또한 이 AbstractAuthenticationProcessingFilter를 상속받고 있다.
그럼 AbstractAuthenticationProcessingFilter 코드의 일부분을 살펴보자.
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException;
// 코드 생략
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 immediately as subclass has indicated that it hasn't completed
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
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) {
// Authentication failed
unsuccessfulAuthentication(request, response, ex);
}
}
// 코드 생략
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);
}
}
doFilter() 메서드 내부에서 attemptAuthentication() 메서드를 실행하는 것을 볼 수 있고, 예외가 발생하지 않고 null이 아닌 Authentication 객체가 반환되었을 때 successfulAuthentication() 메서드가 실행된다.
여기서 successfulAuthentication() 메서드를 통해 파라미터로 받은 Authentication 객체를 SecurityContext에 저장하는 것을 알 수 있다.
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
AuthenticationManager
는 인터페이스로 authentication() 메서드만 구성되어 있다.
실제 AuthenticationManager
의 구현체인 ProviderManager의 authenticate() 메서드를 살펴보자.
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
// 코드 생략
private List<AuthenticationProvider> providers = Collections.emptyList();
private AuthenticationManager parent;
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);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful then it
// will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent
// AuthenticationManager already published it
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed then it will
// publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the
// parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
}
먼저 for문 loop를 통해 AuthenticationProvider들을 모두 살펴보는 것을 알 수 있다.
또한 파라미터로 받은 Authentication 객체를 Provider에게 전달함으로서, 해당 Authentication 객체를 처리할 수 있는 Provider를 찾는 모습을 알 수 있다.
여기서 중점적으로 살펴볼 부분은 AuthenticationProvider의 supports() 메서드와 authentication() 메서드를 통해 Authentication을 처리하는 것을 확인할 수 있다. (27 line과 35 line)
다음으로는 AuthenticationProvider의 supports() 메서드와 authentication() 메서드의 역할을 알아보자.
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
AuthenticationProvider
는 인터페이스로 authentication() 메서드와 supports() 메서드를 구성하고 있다.
AuthenticationProvider
의 구현체를 살펴보며 두 메서드의 역할이 어떤 것인지 확인해보자.
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean, MessageSourceAware {
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(principal,
authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
this.logger.debug("Authenticated user");
return result;
}
// 코드 생략
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
}
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
@Override
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
// 코드 생략
@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"));
}
}
}
supports() 메서드는 파라미터로 받은 Authentication 객체가 UsernamePasswordAuthenticationToken 클래스를 구현한 객체인지 확인하고 맞을 경우에만 인증 로직을 진행한다. (ProviderManager의 authentication() 메서드 참고)
authenticate() 메서드는 인증로직이 포함되어 있고, additionalAuthenticationChecks() 메서드를 통해 Authentication 객체의 credentials(여기서는 password)과 UserDetails를 통해 가져온 credentials 값을 비교하고, 동일 할 경우 createSuccessAuthentication() 메서드를 통해 인증된 UsernamePasswordAuthenticationToken 객체를 만드는 것을 확인할 수 있다.
사용자가 정상적인 username과 password를 가지고 인증 요청했을 시, Spring Security 내부 인증 흐름은 다음과 같다.
AuthenticationFilter
는 인증 요청을 가로채 요청 내부에 있는 username과 password를 바탕으로 Authentication 객체를 생성하고, 생성한 객체를 AuthenticationManager에게 전달한다.AuthenticationFilter
에 반환된다.