[Spring] Security Authentication Flow

유형찬·2022년 11월 21일
0

Code States

목록 보기
20/21

주요 흐름

Spring Security는 Filter를 통해 요청을 처리한다.

🙌Thank you for the image for me. Jung Ha
  1. 로그인 요청
  1. UsernamePasswordAuthenticationFilter가 요청을 처리한다.
  • 클라이언트에서 요청 받은 Username 과 Password를 가지고 Authentication 객체를 생성한다.
  • 여기서 Authentication 객체는 인증을 위한 정보를 가지고 있다. 그러나 인증된 것은 아니다.
  1. Authentication 객체를 생성한다. (UsernamePasswordAuthenticationToken)
  • 인증이 안된 Authentication 객체를 생성한다.
  1. AuthenticationManager를 통해 인증을 처리한다. (ProviderManager)
  • 인증되지 않은 Authentication 객체를 AuthenticationManager에게 전달한다.
  1. AuthenticationProvider를 통해 인증을 처리한다.
  • 여기서 AuthenticationManager는 인터페이스이고 실제 구현체는 ProviderManager이다.
  1. 인증 처리 과정 (UserDetailsService를 통해 사용자 정보를 조회한다.)
  • UserDetails 는 인증된 사용자의 정보를 담고 있는 객체이다.
  • 데이터 베이스 등에서 사용자 정보를 조회하여 UserDetails 객체를 생성한다.
  1. 인증이 성공하면 인증된 Authentication 객체를 생성한다. (UsernamePasswordAuthenticationToken)
  • 인증이 성공하면 인증된 Authentication 객체를 생성한다.
  1. 인증이 성공하면 SecurityContextHolder에 인증된 Authentication 객체를 저장한다.

실제 filter class code

First Filter : UsernamePasswordAuthenticationFilter

  • UsernamePasswordAuthenticationFilter는 인증을 처리하는 필터이다.
  • 가장 먼저 client 의 요청을 마주하는 필터이다.
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    // ...
    public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }
    // ...
}
  • UsernamePasswordAuthenticationFilter는 AbstractAuthenticationProcessingFilter를 상속받는다.
  • 일반적으로 필터 적용을 위해서 doFilter() 메소드를 오버라이딩을 한다.
  • 하지만, 상위 클래스인 AbstractAuthenticationProcessingFilter는 doFilter() 메소드를 오버라이딩 하지 않고 그대로 사용 했다.
  • 현재 코드 내에서 UsernamePasswordAuthenticationFilter는 /login 요청을 처리한다.

Second Filter : AbstractAuthenticationProcessingFilter

  • AbstractAuthenticationProcessingFilter는 인증을 처리하는 필터이다.
  • 실질적인 인증 과정은 하위 클래스에서 구현 하고 있다.
  • 이 필터는 처리된 요청을 SecurityContextHolder에 저장한다.
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean {
    // ...
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        if (requiresAuthentication(request, response)) {
            Authentication authResult;

            try {
                authResult = attemptAuthentication(request, response);
                if (authResult == null) {
                    // return immediately as subclass has indicated that it hasn't completed
                    // authentication
                    return;
                }
                sessionStrategy.onAuthentication(authResult, request, response);
            }
            catch (InternalAuthenticationServiceException failed) {
                logger.error("An internal error occurred while trying to authenticate the user.", failed);
                unsuccessfulAuthentication(request, response, failed);
                return;
            }
            catch (AuthenticationException failed) {
                // Authentication failed
                unsuccessfulAuthentication(request, response, failed);
                return;
            }

            // Authentication success
            if (continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }

            successfulAuthentication(request, response, chain, authResult);
        }
        else {
            // The request match the configured pattern, but failed
            // authentication for some reason. Continue chain with
            // SecurityContextHolder populated with SecurityContext
            // from the session (or a new one if none was present)
            chain.doFilter(request, response);
        }
    }
    // ...
}

위 코드를 살펴보자 Spring Security Config 설정에서 login url 을 /login 으로 설정했기 때문에 /login 요청을 처리한다.

requiresAuthentication()

이 메소드는 client 에서 요청한 url이 /login 이 맞는지 확인한다.
로그인이 필요할 때 /login url로 요청을 보내는데, 이 요청을 처리하기 위해서는 인증이 필요하다.

다른 url로 들어오는 요청은 인증이 필요하지 않기 때문에 /login url로 요청이 들어오면 인증이 필요하다고 판단한다.

다시 말하지만 세션 로그인 과정이다. 타 url로 들어오는 요청 또한 인증된 authentication 객체 정보가 필요 하지만

이미 인증이 되어있기 때문에 인증이 필요하지 않다고 판단한다.

attemptAuthentication()

  • 이 메소드는 인증을 시도한다. 인증이 성공하면 Authentication 객체를 반환하고, 실패하면 AuthenticationException을 발생시킨다.
  • 여기서 시도하는 인증은 UsernamePasswordAuthenticationFilter 를 통해서 시도한다.

successfulAuthentication()

  • 인증이 성공하면 이 메소드가 호출된다.
  • 인증이 성공하면 SecurityContextHolder에 Authentication 객체를 저장한다. ( Session에 저장된다. )

unsuccessfulAuthentication()

  • 인증이 실패하면 이 메소드가 호출된다.
  • 인증이 실패하면 SecurityContextHolder에 Authentication 객체를 저장하지 않는다.
  • AuthenticationFailureHandler 를 통해서 인증 실패에 대한 처리를 한다.

UsernamePasswordAuthenticationToken

  • UsernamePasswordAuthenticationToken은 AbstractAuthenticationToken 을 상속받는다.
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
    // ...
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }
    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, credentials가 있다.
  • principal은 사용자의 식별자이고, credentials는 사용자의 비밀번호이다.
  • 인증이 되지 않은 상태에서 생성할 때는 unauthenticated() 메소드를 사용한다.
  • 인증이 된 상태에서 생성할 때는 authenticated() 메소드를 사용한다.

Authentication

  • 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;
    String getName();
    // ...
}

AuthenticationManager

  • AuthenticationManager는 인증을 담당하는 인터페이스이다.
public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
  • 실질적인 인증은 AuthenticationManager를 구현한 클래스에서 이루어진다.

ProviderManager

  • ProviderManager는 AuthenticationManager를 구현한 클래스이다.

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
    // ...
    private List<AuthenticationProvider> providers = Collections.emptyList();
    // ...
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // ...
        AuthenticationException lastException = null;
        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }
            Authentication result = provider.authenticate(toTest);
            if (result != null) {
                copyDetails(authentication, result);
                return result;
            }
        }
        // ...
    }
    // ...
}

구현체가 하는 역할은 다음과 같다.

  • 인증을 시도할 AuthenticationProvider를 찾는다.
  • AuthenticationProvider를 통해서 인증을 시도한다.
  • 인증이 성공하면 인증된 Authentication 객체를 반환한다.
  • 인증이 실패하면 AuthenticationException을 발생시킨다.

AuthenticationProvider

  • AuthenticationProvider는 인증을 담당하는 인터페이스이다.
public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
    boolean supports(Class<?> authentication);
}

DaoAuthenticationProvider

  • 위 인터페이스를 구현한 클래스 중 하나이다.
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    // ...
    private UserDetailsService userDetailsService;
    // ...
    protected void additionalAuthenticationChecks(UserDetails userDetails,
                                                  UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
        String presentedPassword = authentication.getCredentials().toString();
        if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            logger.debug("Authentication failed: password does not match stored value");
            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
    }
    // ...
}
  • DaoAuthenticationProvider는 인증을 담당하는 클래스이다.
  • 주로 id , password를 이용해서 인증을 시도할 때 사용한다.

UserDetailsService

  • UserDetailsService는 사용자의 정보를 가져오는 인터페이스이다.
public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
  • 실질적인 사용자 정보를 가져오는 역할은 UserDetailsService를 구현한 클래스에서 이루어진다.
  • in Memory 방식으로 사용자 정보를 가져오는 클래스는 InMemoryUserDetailsManager이다.
  • DB를 이용해서 사용자 정보를 가져오는 클래스는 JdbcDaoImpl이다.
  • 여러 방법이 있지만 위 인터페이스를 실제로 구현한 다음 사용하면 된다.

UserDetails

  • UserDetails는 사용자의 정보를 담고 있는 인터페이스이다.
public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    String getPassword();
    String getUsername();
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
}

보통은 위 인터페이스를 구현한 클래스를 만들어서 사용한다.

public class User implements UserDetails {
    // ...
    private final String username;
    private final String password;
    private final Collection<? extends GrantedAuthority> authorities;
    // ...
    public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        if (((username == null) || "".equals(username)) || (password == null)) {
            throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
        }
        this.username = username;
        this.password = password;
        this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
    }
    // ...
}

Authentication vs UserDetails vs Principal

  • Authentication
  • 인증을 담당하는 객체이다.
  • 인증이 성공하면 인증된 Authentication 객체를 반환한다.
  • 인증이 실패하면 AuthenticationException을 발생시킨다.
  • Authentication 객체는 Principal과 Credentials를 가지고 있다.
  • Principal은 인증된 사용자의 정보를 담고 있는 객체이다.
  • Credentials는 인증에 사용된 정보를 담고 있는 객체이다. (보통은 password)
  • Authentication 객체는 SecurityContext에 저장된다.
  • Authentication 객체는 SecurityContextHolder를 통해서 얻을 수 있다.
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

자 그러면 Authentication 객체가 principal과 credentials를 가지고 있는 더 상위의 개념이라고 간단하게 이해할 수 있다.

  • principal == UserDetails 끝 정리다. 용어 세개가 혼동 되어 있어서 이해하기 어려울 수 있으나
  • Authentication 객체가 가진 Principal는 UserDetails 객체이다.
profile
rocoli에요

0개의 댓글