● 이슈 발생 시점
SpringSecurity를 이용해 JWT 인증/인가 절차를 CustomTokenProvider, CustomUserDetailsService를 정의하여 개발하고 있었다.
1. 정상적인 아이디와 비밀번호로 인증 요청이 오면 DB에 유저가 있는지 확인하는 CustomUserDetailsService.loadUserByUsername()이 1번만 실행됨. ( 예상한 로직과 실제 정상적인 로직 )
2. 비정상적인 아이디와 비밀번호로 인증 요청이 오면 CustomUserDetailsService.loadUserByUsername()이 여러 번 실행됨 ( 예상하지 못한 로직과 비정상적인 로직.... )
● 분석
1. Provider의 authenticate는 ProviderManager에서 호출하고 있다.
@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);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful then it
// will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent
// AuthenticationManager already published it
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed then it will
// publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the
// parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
ProviderManager는 등록된 provider를 loop를 돌면서 정상적인 인증 절차가 이루어진 provider가 있으면 loop를 끝낸다.
또, Exception이 발생하게 되면 다음 provider의 인증절차를 계속 진행하게 되고 가장 마지막에 발생한 Exception정보만 가지고 있다.
int size = this.providers.size();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
위 소스 부분에서 providers의 size()가 2개로 잡혀있었다.... 난 분명 CustomProvider 하나만 등록해주었는데....
"DaoAuthenticationProvider"가 추가로 등록되어있었다..... 그 이유를 확인해보자...
● 확인
ProviderManager의 Provider는 SecurityConfig를 작성하면서 "AuthenticationManagerBuilder"객체를 통해서 설정을 해주었다.
그럼 "AuthenticationManagerBuilder"가 빌드를 하면서 ProviderManager가 할당되었으니 빌더 객체를 살펴보았다.
AutneticationManager의 인증 방식에는 총 3가지가 있고 "AuthenticationManagerBuilder"에서도 해당 3가지 방식의 설정 매니저를 적용하는 소스가 있었다. 아래와 같다.
public InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> inMemoryAuthentication()
throws Exception {
return apply(new InMemoryUserDetailsManagerConfigurer<>());
}
....
public JdbcUserDetailsManagerConfigurer<AuthenticationManagerBuilder> jdbcAuthentication() throws Exception {
return apply(new JdbcUserDetailsManagerConfigurer<>());
}
....
public <T extends UserDetailsService> DaoAuthenticationConfigurer<AuthenticationManagerBuilder, T> userDetailsService(
T userDetailsService) throws Exception {
this.defaultUserDetailsService = userDetailsService;
return apply(new DaoAuthenticationConfigurer<>(userDetailsService));
}
userDetailsService를 필요로 하는 DaoDetailsService의 구성 정보를 설정하고 적용해고있다....
SecurityConfig에서 userDetailsService를 등록해주고 있었는데.... Service가 등록되면서 DaoAuthenticationProvider가 생겨난 것이었다...
SecurityConfig.java
@Autowired
public SecurityConfig( JwtTokenProvider jwtTokenProvider
, AuthenticationExceptionHandler authenticationExceptionHandler
, AuthorizationExceptionHandler authorizationExceptionHandler
, AuthenticationManagerBuilder authenticationManagerBuilder
, UserDetailsService userDetailsService) throws Exception {
this.jwtTokenProvider = jwtTokenProvider;
this.authenticationExceptionHandler = authenticationExceptionHandler;
this.authorizationExceptionHandler = authorizationExceptionHandler;
this.authenticationManagerBuilder = authenticationManagerBuilder;
// Provider 추가 설정 기본적으로 DaoAuthenticationProvider가 있음
// Default Provider를 설정함. => Default 실패시 DaoAuthenticationProvider의 authenticate가 실행.
authenticationManagerBuilder.authenticationProvider(jwtTokenProvider);
// DaoAuthenticationProvider가 사용하는 Service로 설정
try {
authenticationManagerBuilder.userDetailsService(userDetailsService);
} catch (Exception e) {
e.printStackTrace();
}
}
userDetailsService는 AuthenticationManagerBuilder에게 등록해줄 필요가 없고, CustomProvider에서 의존성 주입받아서 사용하면 된다.!!
@Autowired
public JwtTokenProvider(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
시큐리티에 대해 디버그를 돌리면 실제 어떻게 돌아가는지 분석하고 있는데 스프링의 세계를 점점 이해하게 되는 것 같다... 끝~~~
'Java > SpringBoot' 카테고리의 다른 글
[SpringBoot] Log4j2 기본 사용법 (0) | 2022.03.07 |
---|---|
[SpringBoot] SOP, CORS 이야기 (0) | 2022.02.03 |
[SpringBoot] UserDetailsService UserNotFoundException 안되는 이유 (0) | 2022.01.11 |
[SpringBoot] 종속성 순환 에러 트러블슈팅 (0) | 2021.12.30 |
[SpringBoot] StereoType, @Component과 @Bean 차이점 (0) | 2021.12.30 |