부트캠프 기록/Section4

[Spring Security] Spring Security 인증 구성요소 이해

bbangduck 2022. 11. 21. 17:29

✅ 학습 목표

  • Spring Security의 인증 처리 흐름을 이해할 수 있다.
  • Spring Security의 핵심 컴포넌트인 인증 컴포넌트의 역할을 이해할 수 있다.

 

Spring Security 인증 처리 흐름

더보기

> 사용자가 로그인 폼 등을 이용해 Username(로그인 ID)과 Password를 포함한 request를 Spring Security가 적용된 애플리케이션에 전송

여러 Filter들 중에서 UsernamePasswordAuthenticationFilter가 해당 요청을 받음

 

> UsernamePasswordAuthenticationFilter는 Username과 Password를 이용해 (2)와 같이 UsernamePasswordAuthenticationToken 을 생성

UsernamePasswordAuthenticationToken은 Authentication 인터페이스를 구현한 구현 클래스

 

아직 인증되지 않은 Authentication 을 가지고있는 UsernamePasswordAuthenticationFilter는 (4)과 같이 해당 Authentication을 AuthenticationManager에게 전달

AuthenticationManager인터페이스를 구현한 구현 클래스가 바로 ProviderManager

 

> ProviderManager 로부터 Authentication을 전달 받은 AuthenticationProvider는 (5)와 같이 UserDetailsService 를 이용해 UserDetails 를 조회

 

> UserDetails 를 생성한 후, 생성된 UserDetails 를 다시 AuthenticationProvider 에게 전달

 

> AuthenticationProvider 는 PasswordEncoder를 이용해 UserDetails 에 포함된 암호화 된 Password와 인증을 위한 Authentication 안에 포함된 Password가 일치하는지 검증

검증에 성공하면 UserDetails를 이용해 인증된 Authentication을 생성

검증에 실패하면 Exception을 발생시키고 인증 처리를 중단

인증된 Authentication을 ProviderManager 에게 전달

 

> 인증된 Authentication을 다시 UsernamePasswordAuthenticationFilter에게 전달

 

> SecurityContextHolder를 이용해 SecurityContext 에 인증된 Authentication을 저장

 


 

 

Spring Security의 인증 컴포넌트

1. UsernamePasswordAuthenticationFilter

더보기
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");

	private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;

	private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

	private boolean postOnly = true;

	public UsernamePasswordAuthenticationFilter() {
		super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
	}

	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);
		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);
	}

	/**
	 * Enables subclasses to override the composition of the password, such as by
	 * including additional values and a separator.
	 * <p>
	 * This might be used for example if a postcode/zipcode was required in addition to
	 * the password. A delimiter such as a pipe (|) should be used to separate the
	 * password and extended value(s). The <code>AuthenticationDao</code> will need to
	 * generate the expected password in a corresponding manner.
	 * </p>
	 * @param request so that request attributes can be retrieved
	 * @return the password that will be presented in the <code>Authentication</code>
	 * request token to the <code>AuthenticationManager</code>
	 */

> AbstractAuthenticationProcessingFilter를 상속

> AbstractAuthenticationProcessingFilter 클래스가 doFilter() 메서드를 포함

> AntPathRequestMatcher클라이언트의 URL에 매치되는 매처

> AntPathRequestMatcher의 객체는상위 클래스인 AbstractAuthenticationProcessingFilter 클래스에 전달되어 Filter가 구체적인 작업을 수행할지 특별한 작업 없이 다른 Filter를 호출할지 결정하는데 사용

 

2. AbstractAuthenticationProcessingFilter

더보기
@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);
   }
}

/**
 * Indicates whether this filter should attempt to process a login request for the
 * current invocation.
 * <p>
 * It strips any parameters from the "path" section of the request URL (such as the
 * jsessionid parameter in <em>https://host/myapp/index.html;jsessionid=blah</em>)
 * before matching against the <code>filterProcessesUrl</code> property.
 * <p>
 * Subclasses may override for special requirements, such as Tapestry integration.
 * @return <code>true</code> if the filter should attempt authentication,
 * <code>false</code> otherwise.
 */
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;
}

/**
 * Performs actual authentication.
 * <p>
 * The implementation should do one of the following:
 * <ol>
 * <li>Return a populated authentication token for the authenticated user, indicating
 * successful authentication</li>
 * <li>Return null, indicating that the authentication process is still in progress.
 * Before returning, the implementation should perform any additional work required to
 * complete the process.</li>
 * <li>Throw an <tt>AuthenticationException</tt> if the authentication process
 * fails</li>
 * </ol>
 * @param request from which to extract parameters and perform the authentication
 * @param response the response, which may be needed if the implementation has to do a
 * redirect as part of a multi-stage authentication process (such as OpenID).
 * @return the authenticated user token, or null if authentication is incomplete.
 * @throws AuthenticationException if authentication fails.
 */
public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
      throws AuthenticationException, IOException, ServletException;

/**
 * Default behaviour for successful authentication.
 * <ol>
 * <li>Sets the successful <tt>Authentication</tt> object on the
 * {@link SecurityContextHolder}</li>
 * <li>Informs the configured <tt>RememberMeServices</tt> of the successful login</li>
 * <li>Fires an {@link InteractiveAuthenticationSuccessEvent} via the configured
 * <tt>ApplicationEventPublisher</tt></li>
 * <li>Delegates additional behaviour to the
 * {@link AuthenticationSuccessHandler}.</li>
 * </ol>
 *
 * Subclasses can override this method to continue the {@link FilterChain} after
 * successful authentication.
 * @param request
 * @param response
 * @param chain
 * @param authResult the object returned from the <tt>attemptAuthentication</tt>
 * method.
 * @throws IOException
 * @throws ServletException
 */
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);
}

/**
 * Default behaviour for unsuccessful authentication.
 * <ol>
 * <li>Clears the {@link SecurityContextHolder}</li>
 * <li>Stores the exception in the session (if it exists or
 * <tt>allowSesssionCreation</tt> is set to <tt>true</tt>)</li>
 * <li>Informs the configured <tt>RememberMeServices</tt> of the failed login</li>
 * <li>Delegates additional behaviour to the
 * {@link AuthenticationFailureHandler}.</li>
 * </ol>
 */
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);
}

> UsernamePasswordAuthenticationFilter가 상속하는 상위 클래스로써 Spring Security에서 제공하는 Filter 중 하나

> HTTP 기반의 인증 요청을 처리하지만 실질적인 인증 시도는 하위 클래스에 맡기고, 인증에 성공하면 인증된 사용자의 정보를 SecurityContext에 저장

 

3. UsernamePasswordAuthenticationToken

더보기
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	private final Object principal;

	private Object credentials;

//생략

/**
 * This factory method can be safely used by any code that wishes to create a
 * unauthenticated <code>UsernamePasswordAuthenticationToken</code>.
 * @param principal
 * @param credentials
 * @return UsernamePasswordAuthenticationToken with false isAuthenticated() result
 *
 * @since 5.7
 */
public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
   return new UsernamePasswordAuthenticationToken(principal, credentials);
}

/**
 * This factory method can be safely used by any code that wishes to create a
 * authenticated <code>UsernamePasswordAuthenticationToken</code>.
 * @param principal
 * @param credentials
 * @return UsernamePasswordAuthenticationToken with true isAuthenticated() result
 *
 * @since 5.7
 */
public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials,
      Collection<? extends GrantedAuthority> authorities) {
   return new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
}

//생략
}

> Spring Security에서 Username/Password로 인증을 수행하기 위해 필요한 토큰이며, 인증 성공 후 인증에 성공한 사용자의 인증 정보가 UsernamePasswordAuthenticationToken에 포함되어 Authentication 객체 형태로 SecurityContext에 저장

 

> UsernamePasswordAuthenticationToken은 AbstractAuthenticationToken 추상 클래스를 상속하는 확장 클래스이자 Authentication 인터페이스의 메서드 일부를 구현하는 구현 클래스

 

4. 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;
}

> 인증을 위해 생성되는 인증 토큰 또는 인증 성공 후 생성되는 토큰UsernamePasswordAuthenticationToken과 같은 하위 클래스의 형태로 생성되지만 생성된 토큰을 리턴 받거나 SecurityContext에 저장될 경우에 Authentication 형태로 리턴 받거나 저장

 

5. AuthenticationManager

public interface AuthenticationManager {

	Authentication authenticate(Authentication authentication) throws AuthenticationException;

}

 

6. ProviderManager

더보기
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;
		}
    
    ...
    ...
	}

  ...
  ...
}

AuthenticationProvider를 관리하고, AuthenticationProvider에게 인증 처리를 위임

 

7. AuthenticationProvider

AuthenticationManager로부터 인증 처리를 위임 받아 실질적인 인증 수행을 담당하는 컴포넌트

하위 클래스가 UserDetailsService로부터 전달 받은 UserDetails를 이용해 인증을 처리

 

8. UserDetails

AuthenticationProviderUserDetails를 이용해 자격 증명을 수행

 

9. UserDetailsService

UserDetails를 로드(load)하는 핵심 인터페이스

 

10. SecurityContext와 SecurityContextHolder

 

SecurityConteext: 인증된 Authentication 객체를 저장하는 컴포넌트SecurityContextHolder: SecurityContext를 관리하는 역할 담당

 

 

 

 

 

참고

https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html

https://www.javainuse.com/webseries/spring-security-jwt/chap3

 

심화 학습

더보기