프레임워크(Framework)/Spring

[Spring Security] 스프링 시큐리티 인증 처리 흐름

잇트루 2023. 1. 1. 22:49
반응형

스프링 시큐리티의 인증 처리 흐름

스프링 시큐리티에서는 스프링 시큐리티 필터 체인을 통해 보안을 위한 특정 작업을 처리한다.

 

다음은 사용자가 로그인 인증을 위한 요청을 할 경우, 스프링 시큐리티에서 해당 인증 요청을 어떻게 처리하는지에 대한 핵심 컴포넌트들의 인증 처리 흐름이다.

1. 사용자가 로그인을 하기 위해 아이디와 패스워드를 포함한 요청(request)을 보낸다.

  • 사용자 로그인 요청이 들어오면 UsernamePasswordAuthenticationFilter가 해당 요청을 전달받는다.

2. UsernamePasswordAuthenticationFilter는 전달받은 Username과 Password를 이용하여 UsernamePasswordAuthenticationToken을 생성한다.

  • UsernamePasswordAuthenticationToken은 Authentication 인터페이스의 구현체이다.
  • 이때 Authentication은 아직 인증이 되지 않은 Authentication이다.

 

3. UsernamePasswordAuthenticationFilter는 Authentication을 AuthenticationManager에 전달한다.

  • AuthenticationManager는 인증 처리를 총괄하는 역할을 하는 인터페이스이다.
  • AuthenticationManager의 구현체는 ProviderManager 클래스이다.

 

4. ProviderManager는 AuthenticationProvider에게 Authentication을 전달한다.

  • UserDetails는 데이터베이스 등의 저장소에 저장된 Username과 사용자의 크리덴셜(Credential)인 Password, 사용자의 권한 정보를 포함하고 있는 컴포넌트이다.

 

5. AuthenticationProvider는 UserDetailsService를 이용하여 UserDetails를 조회한다.

 

6. UserDetailsService는 데이터베이스 등의 저장소에서 사용자의 크리덴셜을 포함한 사용자의 정보를 조회한다.

 

7. 데이터베이스 등의 저장소에서 조회한 사용자의 크리덴셜을 포함한 사용자의 정보를 기반으로 UserDetails를 생성한다.

 

8. 생성된 UserDetails를 AuthenticationProvider에게 전달한다.

 

9. UserDetails를 전달받은 AuthenticationProvider는 PasswordEncoder를 이용해 UserDetails에 포함된 암호화된 Password와 인증을 위한 Authentication 안에 포함된 Password가 일치하는지 검증한다.

  • 검증에 성공하면 UserDetails를 이용해 인증된 Authentication을 생성한다.
  • 검증에 성공하지 못하면 Exception을 발생시키고 인증 처리를 중단한다.

 

10. AuthenticationProvider는 인증된 Authentication을 ProviderManager에게 전달한다.

  • 이때 Authentication은 인증에 성공한 사용자 정보(Principal, Credential, GrantedAuthorities)를 가지고 있다.

 

11. ProviderManager는 인증된 Authentication을 UsernamePasswordAuthenticationFilter에게 전달한다.

 

12. 마지막으로 UsernamePasswordAuthenticationFilter는 SecurityContextHolder를 이용하여 SecurityContext에 인증된 Authentication을 저장한다.

 

UsernamePasswordAuthenticationFilter

사용자가 로그인 요청(requset)을 보내면 가장 먼저 만나는 컴포넌트는 스프링 시큐리티 필터 체인의 UsernamePasswordAuthenticationFilter이다.

 

UsernamePasswordAuthenticationFilter는 전달받은 Username과 Password를 통해 인증을 처리하는 필터이다.

 

UsernamePasswordAuthenticationFilter는 클라이언트로부터 전달받은 Username과 Password를 스프링 시큐리티가 인증 프로세스에서 이용할 수 있도록 UsernamePasswordAuthenticationToken을 생성한다.

 

UsernamePasswordAuthenticationFilter

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { // (1)

    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username"; // (2)

    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password"; // (3)

    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login","POST"); // (4)

    ...
    ...

    public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager); // (5)
    }

    // (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);
        ...

        // (6-2)
        UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
        ...

        return this.getAuthenticationManager().authenticate(authRequest); // (6-3)
    }

    ...
    ...
}
  • (1)에서 UsernamePasswordAuthenticationFilter는 AbstractAuthenticationProcessingFilter 클래스의 상속받는 것을 알 수 있다.
    • AbstractAuthenticationProcessingFilter 클래스 내부에 doFilter() 메서드가 존재한다.

 

  • (2)와 (3)을 통해 클라이언트의 요청 파라미터의 기본 이름이 username과 password라는 것을 알 수 있다.

 

  • (4) AntPathRequestMatcher는 클라이언트의 URL에 매치되는 매처이다.
    • URL이 /login이고 HTTP 메서드가 POST인 경우 매치되도록 한다.

 

  • (5)에서 상위 클래스인 AbstractAuthenticationProcessingFilter에 AntPathRequestMatcher의 객체가 전달된다.
    • AbstractAuthenticationProcessingFilter에서는 필터가 구체적인 작업을 수행할지, 다음 필터를 호출할지 결정하는 데 사용한다.

 

  • (6)의 attemptAuthentication() 메서드는 클라이언트에서 전달한 username과 password 정보를 이용해 인증을 시도하는 메서드이다.
    • AbstractAuthenticationProcessingFilter의 doFilter()에 의해 호출되는 메서드이다.
    • (6-1) HTTP 메서드가 POST가 아니면 Exception을 던진다.
    • (6-2) 클라이언트에서 전달한 username과 password 정보를 이용해 UsernamePasswordAuthenticationToken을 생성한다.
      • UsernamePasswordAuthenticationToken은 인증을 하기 위한 토큰이며, 인증에 성공한 인증 토큰은 아니다.
    • (6-3) AuthenticationManager의 authenticate() 메서드를 호출하여 인증 처리를 위임한다.

 

 

AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter 클래스는 UsernamePasswordAuthenticationFilter의 상위 클래스로 스프링 시큐리티에서 제공하는 필터(Filter) 중 하나이다.

 

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

 

AbstractAuthenticationProcessingFilter

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

    // (1)
    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
    	// (1-1)
        if (!requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
            return;
        }
        try {
            Authentication authenticationResult = attemptAuthentication(request, response); // (1-2)
            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); // (1-3)
        }
        catch (InternalAuthenticationServiceException failed) {
            this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
            unsuccessfulAuthentication(request, response, failed);  // (1-4)
        }
        catch (AuthenticationException ex) {
            // Authentication failed
            unsuccessfulAuthentication(request, response, ex);
        }
    }


    // (2)
    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;
    }

    ...
    ...

    // (3)
    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);
    }


    // (4)
    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);
    }

    ...
    ...
}
  • (1) doFilter() 메서드를 통해 스프링 시큐리티의 필터 역할을 한다.
    • (1-1)에서 AbstractAuthenticationProcessingFilter 클래스가 상황에 따라 인증 처리를 할지 다음 Filter를 호출할지 여부를 결정한다.
    • (1-2)에서는 UsernamePasswordAuthenticationFilter에 인증을 시도하도록 attemptAuthentication() 메서드를 호출하고 있다.
    • (1-3)에서 인증에 성공하면 처리할 동작을 수행하는 successfulAuthentication() 메서드를 호출한다.
    • (1-4)에서는 인증에 실패한 경우 처리할 동작을 수행하는 unsuccessfulAuthentication() 메서드를 호출한다.

 

  • (2) requiresAuthentication() 메서드는 인증 처리 여부를 결정하는 메서드이다.
    • 하위 클래스에서 전달받은 AntPathRequestMatcher의 조건과 매칭될 경우 true를 반환한다.

 

  • (3) 인증에 성공한 경우(1-3) 구체적인 처리 동작이 작성되어 있는 메서드이다.
    • SecurityContextHolder를 통해 SecurityContext에 저장한 뒤, SecurityContext를 HttpSession에 저장한다.

 

  • (4) 인증에 실패한 경우(1-4) 구체적인 처리 동작이 작성되어 있는 메서드이다.
    • SecurityContext를 초기화하고 실패에 대한 로그와 AuthenticationFailureHandler를 호출한다.

 

 

UsernamePasswordAuthenticationToken

AbstractAuthenticationToken 클래스를 상속받는 UsernamePasswordAuthenticationToken은 스프링 시큐리티에서 Username과 Password로 인증하기 위해 필요한 토큰이다.

인증에 성공하면 사용자의 인증 정보가 UsernamePasswordAuthenticationToken에 포함되어 Authentication 객체 형태로 SecurityContext에 저장된다.

 

UsernamePasswordAuthenticationTokendms

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

    ...

    private final Object principal;

    private Object credentials;

    ...


    // (1)
    public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
        return new UsernamePasswordAuthenticationToken(principal, credentials);
    }


    // (2)
    public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials,
            Collection<? extends GrantedAuthority> authorities) {
        return new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
    }

    ...

}
  • 필드에 선언된 principal은 Username 등의 사용자의 신원을 의미한다.

 

  • 필드에 선언된 credentials은 Password를 의미한다.

 

  • (1) unauthenticated() 메서드는 인증을 위한 UsernamePasswordAuthenticationToken 객체를 생성한다.
    • 이때 생성되는 UsernamePasswordAuthenticationToken토큰은 인증에 성공한 토큰은 아니다.

 

  • (2) authenticated() 메서드는 인증에 성공한 이후 SecurityContext에 저장될 UsernamePasswordAuthenticationToken 객체를 생성한다.

 

 

Authentication

Authentication은 스프링 시큐리티에서의 인증을 나타내는 인터페이스이다.

UsernamePasswordAuthenticationToken은 Authentication 인터페이스의 메서드 일부를 구현하는 구현 클래스이기도 하다.

  • Object getCredentials();
  • Object getPrincipal();
  • void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;

 

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

Authentication 인터페이스를 구현한 구현 클래스들은 다음과 같은 정보를 가지고 있다.

Principal

  • 사용자를 식별하는 고유 정보
  • Username과 Password의 경우 Username이 Principal이 된다.
  • 다른 인증 방식에서는 UserDetails가 Principal이 된다.

 

Credentials

  • 사용자 인증에 필요한 Password를 의미한다.
  • 인증이 이루어지고 나면 ProviderManager가 해당 Credentials를 삭제한다.

 

Authorities

  • AuthenticationProvider에 의해 부여된 사용자 접근 권한 목록이다.

 

 

AuthenticationManager

AuthenticationManager는 인증 처리를 총괄하는 인터페이스이다.

public interface AuthenticationManager {

	Authentication authenticate(Authentication authentication) throws AuthenticationException;

}

authenticate() 메서드만 정의되어 있다.

따라서 인증을 위한 필터는 AuthenticationManager를 통해 느슨한 결합을 유지한다.

인증을 위한 실질적인 처리는 AuthenticationManager를 구현하는 구현 클래스에서 이루어진다.

 

ProviderManager

ProviderManager는 AuthenticationManger를 구현하는 대표적인 구현 클래스이다.

AuthenticationProvider를 관리하고, AuthenticationProvider에게 인증 처리를 위임하는 역할을 한다.

 

ProviderManager

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
    ...

    // (1)
    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();

    	// (2)
        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);  // (3)
                if (result != null) {
                    copyDetails(authentication, result);
                    break;
            ...
        }

    	...

        if (result != null) {
            if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
                ((CredentialsContainer) result).eraseCredentials(); // (4)
    	...
    }

    ...
}
  • (1) ProviderManager 클래스는 AuthenticationProvider 객체를 리스트 형태로 의존관계 주입(DI) 받고 있다.
  • (2) 반복문을 통해 적절한 AuthenticationProvider를 찾는다.
  • (3) 적절한 AuthenticationProvider를 찾은 경우 AuthenticationProvider의 authenticate() 메서드를 호출하여 인증 처리 로직을 실행한다.
  • (4) 정상적으로 인증될 경우 인증에 사용한 Credentials를 제거한다.

 

 

AuthenticationProvider

AuthenticationProvider는 AuthenticationManager로부터 인증 처리 위임받아 실질적인 인증 처리를 수행하는 컴포넌트이다.

Username과 Password의 인증 처리는 DaoAuthenticationProvider가 담당하고 있다.

DaoAuthenticationProvider는 UserDetailsService로부터 전달받은 UserDetails를 이용해 인증을 처리한다.

 

DaoAuthenticationProvider

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { // (1)
    ...

    private PasswordEncoder passwordEncoder;

    ...

    // (2)
    @Override
    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        prepareTimingAttackProtection();
        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); // (2-1)
            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);
        }
    }

    // (3)
    @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())) { // (3-1)
            this.logger.debug("Failed to authenticate since password does not match stored value");
            throw new BadCredentialsException(this.messages
                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
    }

    ...
}
  • (1) DaoAuthenticationProvider는 AbstractUserDetailsAuthenticationProvider를 상속받는다.
    • AuthenticationProvider 인터페이스의 구현 클래스는 AbstractUserDetailsAuthenticationProvider이고,
    • DaoAuthenticationProvider는 AbstractUserDetailsAuthenticationProvider를 상속받은 확장 클래스이다.
    • 실질적인 인증 처리는 AbstractUserDetailsAuthenticationProvider 추상 클래스의 authenticate() 메서드에서부터 시작된다.

 

  • (2) retrieveUser() 메서드는 UserDetailsService로부터 UserDetails를 조회하는 역할을 한다.
    • UserDetails는 사용자를 인증하는 데 사용되고 인증된 Authentication 객체를 생성하는 데 사용된다.
    • (2-1)에서 getUserDetailsService() 메서드를 통해 UserDetails를 조회하고 있다.

 

  • (3) additionalAuthenticationChecks() 메서드는 PasswordEncoder를 이용해 사용자의 패스워드를 검증한다.

 

 

UserDetails

UserDetails는 데이터베이스 등의 저장소에 저장된 사용자의 Username과 Credential인 Password와 사용자 권한 정보를 포함하는 컴포넌트이다.

AuthenticationProvider가 UserDetails를 이용하여 자격 증명을 수행한다.

 

UserDetails

public interface UserDetails extends Serializable {

    Collection<? extends GrantedAuthority> getAuthorities();
    String getPassword();
    String getUsername();

    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
}

UserDetails는 사용자 권한 정보, Password, Username 등의 사용자 정보를 포함한다.

  • getAuthorities() : 사용자 권한 정보
  • getPassword() : Password
  • getUsername() : Username

그 외 사용자 계정의 만료 여부, 잠김 여부, Credentials 만료 여부, 활성화 여부 등의 정보도 포함하고 있다.

 

 

UserDetailsService

UserDetailsService는 UserDetails를 불러오는(Load) 핵심 인터페이스이다.

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

UserDetailsService 인터페이스는 loadUserByUsername() 메서드를 정의하고 있다.

  • UserDetailsService를 구현하는 구현 클래스에서 loadUserByUsername() 메서드를 통해 사용자의 정보를 불러온다.
  • 사용자의 정보를 어디에서 불러오는지는 애플리케이션에서 사용자의 정보를 어디에서 관리(DB, 메모리 등)하고 있는지에 따라 달라진다.

 

SecurityContext와 SecurityContextHolder

SecurityContext는 인증된 Authentication 객체를 저장하는 컴포넌트이다.

SecurityContextHolder는 SecurityContext를 관리하는 역할을 담당한다.

  • SecurityContext가 인증된 Authentication 객체를 포함하고 있으며, SecurityContext는 SecurityContextHolder에 속해있다.
  • SecurityContextHolder를 통해 인증된 Authentication을 SecurityContext에 설정할 수 있고, SecurityContextHolder를 통해 Authentication 객체에 접근할 수 있다는 것을 의미한다.

 

SecurityContextHolder

public class SecurityContextHolder {
    ...

    private static SecurityContextHolderStrategy strategy;

    ...

    public static SecurityContext getContext() {
        return strategy.getContext();
    }

    ...

    public static void setContext(SecurityContext context) {
        strategy.setContext(context);
    }

    ...
}
  • getContext() 메서드를 통해 SecurityContext를 얻을 수 있다.
  • setContext() 메서드를 통해 인증된 Authentication 객체를 포함한 SecurityContext를 설정할 수 있다.
반응형