SpringSecurity

SpringSecurity [AuthenticationManager]

성찬우 2022. 6. 24. 22:23

Authentication 을 만들고 인증을 처리하는 인터페이스 AuthenticationManager에 대해서 알아 보겠다. 

 

잠시 확인을 하자면

 

ContextHolder 는 Authentication 의 정보를 담고있는 것이고.

AuthenticationManager는 실제 인증을 담당한다. 

 

인증 하는 동안 무슨일이 일어나는지 알아보겠다. 

 

 

 

AuthenticationManager는 authenticate 라는 메서드 하나를 갖는다. 매우 조촐한데 

Authentication 객체를 파라미터를 받고있으며 이 객체는 정보를 담고 있다. (user가 보낸 username, password등 )

public interface AuthenticationManager {
   Authentication authenticate(Authentication authentication) throws AuthenticationException;

}

인터페이스이기 때문에 우리는 구현체를 보통 가져다가 쓴다. 바로 

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

ProviderManager 이다. 물론 AuthenticationManager 를 implements받아서 사용해도 되지만 보통 이렇게 사용한다. 

 

 

ProviderManager 클래스 안에는 

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {

이렇게 @Override 되어있는데 여기를 한번 디버거를 통해 어떤 정보들이 오고가는지 확인해보고자 한다. 

 

 

 

디버거를 통해 로그인을 실행했을 때 

authentication 이라는 이름으로 내가 입력한 정보들이 담겨있는 것을 볼 수 있다. 

username = user / cksdntjd (여기서 유저인데 중간에 cksdntjd으로 바뀜... 혼자 테스트하다가 꼬였음 ㅠㅠ)

password = 123

해당 authentication 은 

UsernamePasswordAuthenticationToken 이다. 하지만 

@Override된 메서드를 쭉 보다보면 

AuthenticationProvider 는 UsernamePasswordAuthenticationToken 을 가지고있지 않고 AnonymousAuthenticationProvider를 가지고있다. 즉 처리를 못한다는 것이다. 

 

이후 다음 if문에서 걸리게 되고 

해당 if문을 try 문을 통해서 parent 로 들어가게 된다. 

이번에는 

이 provider는 새로운 provider다 햇갈리지 말자

 

AnonymousAuthenticationProvider 가 아닌 DaoAuthenticationProvider가 등장했다. 

아까는 여기서 걸리게 되었지만 이번에는 어떻게 되는지 확인해 보자 

 

이렇게 내부에서 UsernamePasswordAuthenticationToken 을 처리할 수 있다. . 

계속 저 support 메소드가 신경 쓰이는데 
AnonymousAuthenticationProvider.class 를 받는 support 메서드와
UsernamePasswordAuthenticationToken.class 를 받는 support 메서드로 구분되어있다 . 
부모 클래스가 UsernamePasswordAuthenticationToken클래스를 받는다.

 

이렇게 결국 result 에 authenticate 메서드를 이용해서 넣을 수가 있다. 

여기서 쓰이는 authenticate 메서드는 

AbstractUserDetailsAuthenticationProvider.class

에 존재한다. 

 

계속해서 정보를 옮기는 중인건데 

그래서 이게 언제 인증을 해주는것 인지 확인을 해야한다. 

저 retrieveUser  메서드가 포인트다 드디어 우리의 코드와 연결되는 구간이다. 

 

내가 적은 유저의 아이디가 노출이 된다.  또 들어가보자 

DaoAuthenticationProvider.class

로 들어오게 되는데 

여기서 저 getUserDetailsService().loadUserByname(username); 이 매우매우 익숙하다. 

 

@Service
public class AccountService implements UserDetailsService {

여기에 선언해놓은 

유저의 정보들이다. 결국 여기서 User의 정보 (security의 유저임)

import org.springframework.security.core.userdetails.User;

가 return 되는 것을 알수 있다. 

 

결국 최종적으로 

 

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;  //여기여기
   }

우리는 result 를 반환하는데 

result 는 첫번째 try문에서 선언이 되고 [  result = provider.authenticate(authentication); ]

 

결국 이  result 는 Authentication 객체이다. 

 

이 Authentication 은 인증 과정을 계속 해서 거치다가. 완료 되면 

SecurityContextHolder

로 들어오게 되고 

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

해당 Authentication 객체로 꺼내 유저의 정보를 우리가 코드로 꺼내 사용 가능한 것이다.