1.인증 컴포넌트를 이용한 인증 처리 |
-기본 part 2의 인증 처리 흐름을 보다 자세히 이해하기 위해 Spring Security에서 지원하는 인증 컴포넌트들의 내부 코드를 들여다볼 예정임
[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)에서 알 수 있듯 DaoAuthenticationProvider는 AbstractUserDetailsAuthenticationProvider를 상속한 확장 클래스이므로 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에 값이 채워져 있다면 인증된 사용자로 간주함
● 위의 그림과 같이 SecurityContextHolder가 SecurityContext를 포함하고 있는 것은 SecurityContextHolder를 통해 인증된 Authentication을 SecurityContext에 설정할 수 있고 또한 SecurityContextHolder를 통해 인증된 Authentication 객체에 접근할 수 있다는 것을 의미함
-이미지 및 내용 출처 : code states
'부트캠프' 카테고리의 다른 글
Spirng Security - JWT와 OAuth2 기초 (0) | 2023.03.29 |
---|---|
Spring Security - 권한 부여 컴포넌트 (0) | 2023.03.22 |
Spring Security - 기본 part 2 (0) | 2023.03.21 |
Spring Security - DelegatingPasswordEncoder (0) | 2023.03.21 |
Spring Security - Filter 와 Filter Chain (1) | 2023.03.21 |