[Spring] Spring Security Authentication

Gogh·2023년 1월 2일
0

Spring

목록 보기
19/23

🎯 목표 : Spring Security 인증 처리 Flow 와 Component 에 대한 이해

📒 Spring Security Authentication Flow

image

📌 인증 처리 흐름

  • Spring Security의 컴포넌트들이 어떤 과정을 거쳐 인증 요청을 처리하는지 정리해 보았다. 위 그림을 기반으로 정리했다.
    1. Username, Password 를 포함한 Request Data가 서버에 전송된다.
    2. AbstractAuthenticationProcessingFilter를 상속받은 UsernamePasswordAuthenticationFilter에서 Username 과 Password를 이용하여 UsernamePasswordAuthenticationToken을 생성한다. 이때, UsernamePasswordAuthenticationTokenAuthentication인터페이스를 구현한 클래스며, 아직 인증이 완료 되지 않은 Authentication이다.
    3. 인증되지 않은 Authentication(= UsernamePasswordAuthenticationToken)UsernamePasswordAuthenticationFilter는 인증 처리를 총괄하는 인터페이스 AuthenticationManager를 구현한 ProviderManager에게 전달 한다.
    4. ProviderManager는 직접 인증처리를 하는 것이 아니라 인증 처리를 할 컴포넌트를 찾는데, 인증 처리를 직접적으로 하는 컴포넌트가 바로, AuthenticationProvider다.
    5. Authentication를 전달받은 AuthenticationProvider는 암호화로 저장되어 있는 Password 를 조회하기 위해 PasswordEncoder를 통해 Authentication의 Password 를 인코딩하여 다시 전달 받는다.
    6. AuthenticationProviderUserDetailsServiceUserDetails를 조회 한다. UserDetails는 DB등 저장소에 저장된 사용자의 Username과 사용자의 자격을 증명해주는 크리덴셜인 Password 그리고 사용자의 권한 정보를 포함하고 있는 컴포넌트다.
    7. UserDetailsService는 저장소에서 사용자를 조회하여 조회한 사용자의 크리덴셜등 정보를 기반으로 UserDetails를 생성한다.
    8. UserDetailsService는 생성한 UserDetailsAuthenticationProvider에게 전달한다. 전달 받은 정보를 가지고 AuthenticationProviderPasswordEncoder를 이용하여 UserDetails와 인증을 위한 Authentication 내의 Password(5 에서 전달받아놓은 인코딩 Password)가 일치하는지 검증한다.
      • 검증에 성공하면 인증된 Authentication을 생성한다.
      • 실패하면 Exception 발생으로 인증 처리를 중단한다.
    9. AuthenticationProvider는 인증된 AuthenticationProviderManager에게 전달한다.
    10. ProviderManager는 인증된 AuthenticationUsernamePasswordAuthenticationFilter에게 전달한다.
    11. 인증된 AuthenticationUsernamePasswordAuthenticationFilterSecurityContext에 저장한다.
      • 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);
	}

  // (6)
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
    // (6-1)
		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는 usernamepassword다.
  • AntPathRequestMatcher는 클라이언트의 URL에 매치되는 객체다. "/login"POST메소드일 경우 매치되는 것을 알수 있다.
  • 생성자로 AntPathRequestMatcherAuthenticationManager를 상위 클래스에 전달하는 것을 알수 있다.
  • 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 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 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

  • 인증 자체를 표현하는 인터페이스다.
  • UsernamePasswordAuthenticationTokenAbstractAuthenticationToken 추상 클래스를 상속하는 클래스이며, 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"));
    }
  }

  //...
    //...
}
  • DaoAuthenticationProviderAbstractUserDetailsAuthenticationProvider를 상속 받는다.
    • AuthenticationProvider의 구현한 추상 클래스가 AbstractUserDetailsAuthenticationProvider이고, 그것을 확장한 클래스가 DaoAuthenticationProvider다.
    • 즉, 추상 클래스의 authenticate() 메소드로 부터 실제 인증 처리가 시작된다.
  • retrieveUser()UserDetailsService로부터 UserDetails를 조회하는 역할을 한다. 이는 사용자를 인증하는데 사용되며, 인증에 성공할 경우 인증된 Authentication 객체를 생성하는데 사용된다.
  • additionalAuthenticationChecks()에서는 PasswordEncoder를 이용하여 사용자의 패스워드를 검증하고 있다.
  • 확장 클래스와 구현 추상 클래스의 메소드 호출 순서 요약
    • AbstractUserDetailsAuthenticationProvider authenticated() 메서드 호출
    • DaoAuthenticationProvider retrieveUser() 메서드 호출
    • DaoAuthenticationProvider additionalAuthenticationChecks() 메서드 호출
    • DaoAuthenticationProvider createSuccessAuthentication() 메서드 호출
    • AbstractUserDetailsAuthenticationProvider createSuccessAuthentication() 메서드 호출
    • 인증된 AuthenticationProviderManager에게 리턴

📌 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

image

  • SecurityContextHolderSecurityContext를 포함하고 있고 SecurityContextAuthentication를 포함하고 있다.
  • 즉, SecurityContextHolder를 통해 인증된 AuthenticationSecurityContext에 설정할 수 있고 객체에 접근할 수 있다는 뜻이다.
  • SecurityContextHolder의 기본 전략은 ThreadLocalSecurityContextHolderStrategy다.
    • ThreadLocal은 쓰레드 간에 공유되지 않는 쓰레드 고유의 로컬 영역을 말한다.
    • WebMVC 기반 프로젝트는 일반적인 경우 요청 하나에 쓰레드 하나를 생성한다.
    • 쓰레드 마다 고유한 공간을 만들수 있고 그곳에 SecurityContext를 저장한다.
  • getContext() 메소드로 현재 실행 쓰레드의 SecurityContext를 얻을 수 있다.
  • setContext() 메소드는 현재 쓰레드에 SecurityContext를 연결한다. 일반적으로 인증된 Authentication을 포함한 SecurityContext를 연결하는데 사용한다.
profile
컴퓨터가 할일은 컴퓨터가

0개의 댓글