Spring security - AuthenticationProvider
AuthenticationProvider
AuthenticationProvider는 interface로 사용자가 상황에 맞는 인증방식을 구현하기 위해 구현한다.
두 가지 함수가 정의되어있는데
- authenticate
- supports
이다.
유저가 name, password를 입력하여 전달하면 AuthenticationProvider를 상속받아 구현하는 class의 support 함수를 호출한다.
support 함수는 Authentication interface class중 어떤 하위 구현 클래스를 사용하는지 확인하는 클래스이다.
현재 프로젝트에는 UsernamePasswordAuthenticationToken을 사용하고 있다.
이 후, authenticate 함수가 동작하게 된다.
authenticate 함수 내부에는 어떤 로직을 사용하여 유저가 입력한 정보를 검증할 지 구현한다.
이 때, DB에 있는 값을 사용하기 위해서 Spring security에서 권장하는 방법은 UserDetailsService의 loadUserByUsername 함수를 사용하여 값을 가져오는 것이다.
아래의 flow를 참고하면 Authentication Manager interface class를 기준으로 ProviderManager 구현 class가 존재한다.
그리고 ProviderManager는 AuthenticatinProvider를 상속받아 구현된 여러 클래스를 List로 가지고 있다.
AuthenticatinProvider를 상속받아 구현된 여러 클래스들은 각 상황에 맞게 인증 로직과 어떤 Authentication Token을 지원할 것인지 여부에 대해 구현이 되어있을 것이다. (authenticate, supports)
ProviderManager는 List로 들고있는 여러 AuthenticatinProvider를 for loop으로 돌면서 전달받은 Token의 클래스를 지원하는지 여부를 확인하고(supports 함수) 지원한다면 인증을 진행한다.(authenticate)
자세한 내용은 아래의 코드를 참조하자.
package org.springframework.security.authentication;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.log.LogMessage;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
private static final Log logger = LogFactory.getLog(ProviderManager.class);
private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
private List<AuthenticationProvider> providers = Collections.emptyList();
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private AuthenticationManager parent;
private boolean eraseCredentialsAfterAuthentication = true;
public ProviderManager(AuthenticationProvider... providers) {
this(Arrays.asList(providers), null);
}
public ProviderManager(List<AuthenticationProvider> providers) {
this(providers, null);
}
public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {
Assert.notNull(providers, "providers list cannot be null");
this.providers = providers;
this.parent = parent;
checkState();
}
@Override
public void afterPropertiesSet() {
checkState();
}
private void checkState() {
Assert.isTrue(this.parent != null || !this.providers.isEmpty(),
"A parent AuthenticationManager or a list of AuthenticationProviders is required");
Assert.isTrue(!CollectionUtils.contains(this.providers.iterator(), null),
"providers list cannot contain null values");
}
@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;
}
@SuppressWarnings("deprecation")
private void prepareException(AuthenticationException ex, Authentication auth) {
this.eventPublisher.publishAuthenticationFailure(ex, auth);
}
/**
* Copies the authentication details from a source Authentication object to a
* destination one, provided the latter does not already have one set.
* @param source source authentication
* @param dest the destination authentication object
*/
private void copyDetails(Authentication source, Authentication dest) {
if ((dest instanceof AbstractAuthenticationToken token) && (dest.getDetails() == null)) {
token.setDetails(source.getDetails());
}
}
public List<AuthenticationProvider> getProviders() {
return this.providers;
}
@Override
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
public void setAuthenticationEventPublisher(AuthenticationEventPublisher eventPublisher) {
Assert.notNull(eventPublisher, "AuthenticationEventPublisher cannot be null");
this.eventPublisher = eventPublisher;
}
/**
* If set to, a resulting {@code Authentication} which implements the
* {@code CredentialsContainer} interface will have its
* {@link CredentialsContainer#eraseCredentials() eraseCredentials} method called
* before it is returned from the {@code authenticate()} method.
* @param eraseSecretData set to {@literal false} to retain the credentials data in
* memory. Defaults to {@literal true}.
*/
public void setEraseCredentialsAfterAuthentication(boolean eraseSecretData) {
this.eraseCredentialsAfterAuthentication = eraseSecretData;
}
public boolean isEraseCredentialsAfterAuthentication() {
return this.eraseCredentialsAfterAuthentication;
}
private static final class NullEventPublisher implements AuthenticationEventPublisher {
@Override
public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {
}
@Override
public void publishAuthenticationSuccess(Authentication authentication) {
}
}
}