부트캠프

Spring Security - 인증 컴포넌트

hunm719 2023. 3. 22. 20:35
1.인증 컴포넌트를 이용한 인증 처리

 -기본 part 2의 인증 처리 흐름을 보다 자세히 이해하기 위해 Spring Security에서 지원하는 인증 컴포넌트들의 내부 코드를 들여다볼 예정임

Spring Security의 컴포넌트로 보는 인증(Authentication) 처리 흐름

 

 [1] UsernamePasswordAuthenticationFilter

  ● 위의 그림에서 사용자의 로그인 request를 제일 먼저 만나는 컴포넌트

   일반적으로 로그인 폼에서 제출되는 Username과 Password를 통한 인증을 처리하는 Filter

   클라이언트로부터 전달받은 Username과 Password를 Spring Security가 인증 프로세스에서 이용할 수 있도록 UsernamePasswordAuthenticationToekn을 생성함

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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)
    }
 
    ...
  ...
}
cs

<위의 코드는 UsernamePasswordAuthenticationFilter 클래스의 코드 일부>

   (1)처럼 UsernamePasswordAuthenticationFilter는 AbstractAuthenticationProcessingFilter를 상속함

   (2)(3)을 통해 클라이언트의 로그인 폼을 통해 전송되는 request parameter의 디폴트 name은 username과 password라는 사실을 알 수 있음

  (4)의 AntPathRequestMatcher클라이언트의 URL에 매치되는 매처로, 클라이언트의 URL이 "/login"이고, HTTP Method가 POST 일 경우 매치 될 것이라는 사실을 예측할 수 있음. 또한, AntPathRequestMatcher의 객체(DEFAULT_ANT_PATH_REQUEST_MATCHER)는 (5)에서 상위 클래스인 AbstractAuthenticationProcessingFilter 클래스에 전달되어 Filter가 구체적인 작업을 수행할지 특별한 작업 없이 다른 Filter를 호출할지 결정하는데 사용

   (5)에서 AntPathRequestMatcher의 객체(DEFAULT_ANT_PATH_REQUEST_MATCHER)와 AuthenticationManager를 상위 클래스인 AbstractAuthenticationProcessingFilter에 전달함

   (6)의 attemptAuthentication() 메서드는 클라이언트에서 전달한 username과 password 정보를 이용해 인증을 시도함

      ○ (6-1)에서 HTTP Method가 POST가 아니면 Exception을 throw함

       (6-2)에서 클라이언트에서 전달한 username과 password 정보를 이용해 UsernamePasswordAuthenticationToken을 생성함

       (6-3)에서 AuthenticationManager의 authenticate() 메서드를 호출해 인증 처리를 위임함

 

 

 

 [2] AbstractAuthenticationProcessingFilter

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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);
    }
 
  ...
  ...
}
cs

<위의 코드는 UsernamePasswordAuthenticationFilter 클래스의 코드 일부>

   (1)을 통해 AbstractAuthenticationProcessingFilter 클래스가 Spring Security의 Filter임을 알 수 있음

      ○ (1-1)에서는 클래스가 인증 처리를 해야하는지, 아니면 다음 Filter를 호출할지의 여부를 결정하고 있음. 여기서 호출하는 requiresAuthentication() 메서드는 (2)에서 확인할 수 있듯이 하위 클래스에서 전달받은 requiresAuthenticationRequestMatcher 객체를 통해 들어오는 요청이 인증 처리를 해야 하는지 여부를 결정하고 있음. 매칭조건은 AntPathRequestMatcher("/login","POST")의 파라미터인 URL과 HTTP Method

      ○ (1-2)에서는 하위 클래스(UsernamePasswordAuthenticationFilter)에 인증을 시도해 줄 것을 요청하고 있음.

      ○ (1-3)에서는 인증에 성공하면 successfulAuthentication() 메서드를 호출함. 해당 메서드는 (3)에서 확인할 수 있듯이 인증에 성공한 이후, SecurityContextHolder를 통해 사용자의 인증 정보를 SecurityContext에 저장한 뒤, SecurityContext를 HttpSession에 저장함

      ○ (1-4)에서는 인증에 실패하면 unsucceessfulAuthentication() 메서드를 호출함. (4)에서 확인할 수 있듯이 SeucurityContext를 초기화 하고, AuthenticationFailureHandler 를 호출함.

 

 

 [3] UsernamePasswordAuthenticationToken

   Spring Security에서 Username/Password로 인증을 수행하기 위해 필요한 토큰

   인증 성공 후, 인증에 성공한 사용자의 인증 정보가 토큰에 포함되어 Authentication 객체 형태로 SecurityContext에 저장됨

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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);
    }
 
  ...
  ...
 
}
cs

<위의 코드는 UsernamePasswordAuthenticationToken 클래스의 코드 일부>

   principal 필드는 Username 등의 신원을 의미하고, credential 필드는 Password를 의미함

   (1)의 unauthenticated() 메서드는 인증에 필요한 용도의 UsernamePasswordAuthenticationToken 객체를 생성함

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

 

 

 [4] Authentication

   Spring Security에서의 인증 자체를 표현하는 인터페이스

   애플리케이션의 코드 상에서 인증을 위해 생성되는 인증 토큰 또는 인증 성공 후 생성되는 토큰은 UsernamePasswordAuthenticationToken 같은 하위 클래스의 형태로 생성되지만, 생성된 토큰을 리턴 받거나 SecurityContext에 저장될 경우에 Authentication 형태로 리턴 받거나 저장됨

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

1
2
3
4
5
6
7
8
public interface Authentication extends Principal, Serializable {
    Collection<extends GrantedAuthority> getAuthorities();
    Object getCredentials();
    Object getDetails();
    Object getPrincipal();
    boolean isAuthenticated();
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
cs

<위의 코드는 Authentication 인터페이스의 코드>

   Principal 은 사용자를 식별하는 고유 정보로 일반적으로 Username/Password 기반 인증에서 Username 이 Principal이 되며, 다른 인증 방식에서는 UserDetails 가 Principal이 됨

   Credentails 는 사용자 인증에 필요한 Password를 의미하며 인증이 이루어지고 난 직후, ProviderManager 가 해당 Credentials를 삭제함

   Authorities 는 AuthenticationProvider 에 의해 부여된 사용자의 접근 권한 목록임.

 

 

 [5] AuthenticationManager

   인증 처리를 총괄하는 매니저 역할을 하는 인터페이스

1
2
3
public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
cs

<위의 코드는 AuthenticationManager 인터페이스> 

   authenticate() 메서드 하나만 정의되어 있고, 인증을 위한 Filter는 AuthenticationManager를 통해 느슨한 결합을 유지하고 있으며, 인증을 위한 실질적인 관리는 AuthenticationManager를 구현하는 구현 클래스를 통해 이루어짐

 

 

[6] ProviderManager

   일반적인 Spring Security에서의 AuthenticationManager 인터페이스의 구현 클래스

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

 

 

 [7] AuthenticationProvider

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

   Username/Password 기반의 인증 처리는 DaoAuthenticationProvider가 담당하고, DaoAuthenticationProvider는 UserDetailsService로부터 전달받은 UserDetails를 이용해 인증을 처리함

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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"));
        }
    }
 
  ...
  ...
}
cs

<위의 코드는 AuthenticationProvider 인터페이스의 구현 클래스를 확장하는 DaoAuthenticationProvider 클래스의 코드 일부>

   (1)에서 알 수 있듯 DaoAuthenticationProviderAbstractUserDetailsAuthenticationProvider를 상속한 확장 클래스이므로 AbstractUserDetailsAuthenticationProvider 추상 클래스의 authenticate() 메서드에서부터 실질적인 인증 처리가 시작됨

   (2)의 retrieveUser() 메서드는 UserDetailsService로부터 UserDetails를 조회하는 역할을 하며, 조회된 UserDetails는 사용자를 인증하는 데 사용될 뿐만 아니라 인증에 성공할 경우, 인증된 Authentication 객체를 생성하는 데 사용됨

      ○ (2-1)의 this.getUserDetailsService().loadUserByUsername(username); 에서 UserDetails를 조회함

   (3)의 additionalAuthenticationChecks() 메서드에서 PasswordEncoder를 이용해 사용자의 패스워드를 검증함

      ○ (3-1)에서 클라이언트로부터 전달받은 패스워드와 데이터베이스에서 조회한 패스워드가 일치하는지 검증함

 

 

 [8] UserDetails 와 UserDetailsService

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface UserDetails extends Serializable {
 
    Collection<extends GrantedAuthority> getAuthorities(); // (1) 권한 정보
    String getPassword(); // (2) 패스워드
    String getUsername(); // (3) Username
 
    boolean isAccountNonExpired();  // (4)
    boolean isAccountNonLocked();   // (5)
    boolean isCredentialsNonExpired(); // (6)
    boolean isEnabled();               // (7)
}
 
public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
cs

<위의 코드는 UserDetails 인터페이스와 UserDetailsService 인터페이스 코드>

  UserDetails 인터페이스는 사용자의 권한 정보(1), 패스워드(2), Username(3)을 포함하고 있으며, 사용자 계정의 만료 여부(4) 사용자 계정의 lock 여부(5), Credentials(Password)의 만료 여부(6), 사용자의 활성화 여부(7)에 대한 정보를 포함하고 있음

   UserDetailsService 인터페이스는 loadUserByUsername(String username)을 통해 사용자의 정보를 로드함

 

 

 [9] SecurityContext 와 SecurityContextHolder

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

   SecurityContextHolder는 SecurityContext를 관리하는 역할

   Spring Security 입장에서는 SecurityContextHolder에 의해 SecurityContext에 값이 채워져 있다면 인증된 사용자로 간주함

SecurityContext와 SecurityContextHolder의 구조

   위의 그림과 같이 SecurityContextHolder가 SecurityContext를 포함하고 있는 것은 SecurityContextHolder를 통해 인증된 Authentication을 SecurityContext에 설정할 수 있고 또한 SecurityContextHolder를 통해 인증된 Authentication 객체에 접근할 수 있다는 것을 의미함

 

 

 

 

 

 

-이미지 및 내용 출처 : code states