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();
Iterator var9 = this.getProviders().iterator();
while(var9.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var9.next();
if (provider.supports(toTest)) {
if (logger.isTraceEnabled()) {
Log var10000 = logger;
String var10002 = provider.getClass().getSimpleName();
++currentPosition;
var10000.trace(LogMessage.format("Authenticating request with %s (%d/%d)", var10002, currentPosition, size));
}
try {
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (InternalAuthenticationServiceException | AccountStatusException var14) {
this.prepareException(var14, authentication);
throw var14;
} catch (AuthenticationException var15) {
AuthenticationException ex = var15;
lastException = ex;
}
}
}
if (result == null && this.parent != null) {
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
} catch (ProviderNotFoundException var12) {
} catch (AuthenticationException var13) {
parentException = var13;
lastException = var13;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
} else {
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
this.prepareException((AuthenticationException)lastException, authentication);
}
throw lastException;
}
}
그렇다. 굉장히 길다. 하지만 다 이해할 필요는 없다. 일단은 중요한 부분들만 살펴보며 기본 동작을 알아보자.
0. Authentication 객체는 인증요청 정보를 담고 있다.
public Authentication authenticate(Authentication authentication)
- 인증절차는 사용자가 넘긴 Authentication 객체를 통해 진행되며, 이곳저곳에서 참조되고 수정될 예정이다.
- Authentication = 아이디/패스워드 정도로 기억해두고 보자.
1. ProviderManager는 여러 AuthenticationProvider를 가진다.
Iterator var9 = this.getProviders().iterator(); while(var9.hasNext()) { ... }
(앞으로 AuthenticationProvider를 Provider와 혼용해서 적겠다.)
- Provider들을 가져와서 각 provider들에 대해 반복문을 돌린다.
- 한 ProviderManager는 여러 Provider들을 가질 수 있고, 이를 통해 JwtAuthenticationProvider, DaoAuthenticationProvider 등 원하는 Provider를 끼워넣을 수 있다. (난 스프링을 끼워 넣는 거라고 생각한다...)
2. 각 AuthenticationProvider는 실제로 인증을 수행한다. (중요)
try { result = provider.authenticate(authentication); if (result != null) { this.copyDetails(authentication, result); break; } } catch (InternalAuthenticationServiceException | AccountStatusException var14) { this.prepareException(var14, authentication); throw var14; } catch (AuthenticationException var15) { AuthenticationException ex = var15; lastException = ex; }
- 딱봐도 중요해보이는 provider.authenticate() 메서드가 등장했다.
- provider는 Authentication 객체를 가지고 실제로 인증을 시도하며, Authentication 객체, result를 반환한다.
- 해당 Provider가 Authentication을 핸들링할 수 있다면 인증을 시도한다.
- 시도하여 인증에 성공했다면 result 객체를 세팅하여 반환한다.
- 시도하여 인증에 실패했다면 AuthenticationException을 터트린다.
- 해당 Provider가 Authentication을 핸들링할 수 없다면 다음 Provider에게 기회가 넘어간다.
- AuthenticationException 이 발생했을 경우, 바로 터트리지는 않고, 다른 Provider에게 기회를 주기위해 일단 저장해둔다.
- 결론적으로 시나리오는 다음 3개와 같다.
- 핸들링 성공, 인증 성공 -> Provider 찾아보기 끝내기, Authentication 세팅
- 핸들링 성공, 인증 실패 -> 일단 예외를 저장해두고 다음 Provider에게 기회주기
- 핸들링 실패 -> 다음 Provider에게 기회주기
3. 결과 처리
if (result != null) { if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) { ((CredentialsContainer)result).eraseCredentials(); } if (parentResult == null) { this.eventPublisher.publishAuthenticationSuccess(result); } return result; } else { if (lastException == null) { lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}")); } if (parentException == null) { this.prepareException((AuthenticationException)lastException, authentication); } throw lastException; }
- 모든 Provider를 다 시도해본 후 나오는 코드이다.
- result가 null이 아닐 경우, 즉 인증에 성공했을 경우
- Authentication 객체의 Credential을 지운다. -> HttpSession이 길게 유지되는 것을 방지하기 위해(보안을 위해) 작동된다.
- 인증 성공을 알린다. (SecurityContext에 담기는 과정이다. 다음에 설명이 나온다.)
- result가 null일 경우, 즉 인증에 실패했을 경우
- 대충, 마지막 AuthenticatoinException을 드디어 던지게 된다.
그 후는 어떻게 진행되는가?
이 다음의 인증 절차는 위 코드에서는 나타나지 않지만, 간단하게 요약하면 이렇다.
- 인증이 완료된 Authentication을 SecurityContext에 담는다.
- SecurityContext는 SecurityContextHolder에 담긴다.
- 결론적으로 SecurityContextHolder를 통해 인증된 정보에 접근할 수 있게 된다.
- 이 정보는 후에 인가를 위해 사용된다.
설명을 뛰어넘은 코드 1
if (provider.supports(toTest)) { if (logger.isTraceEnabled()) { Log var10000 = logger; String var10002 = provider.getClass().getSimpleName(); ++currentPosition; var10000.trace(LogMessage.format("Authenticating request with %s (%d/%d)", var10002, currentPosition, size)); } }
- 로깅을 지원하기 위한 코드다.
- 설정파일에 logging.level.org.springframework.security=TRACE 설정을 통해 로그를 볼 수 있다.
설명을 뛰어넘은 코드 2 (중요)
if (result == null && this.parent != null) { try { parentResult = this.parent.authenticate(authentication); result = parentResult; } catch (ProviderNotFoundException var12) { } catch (AuthenticationException var13) { parentException = var13; lastException = var13; } }
- ProviderManager는 다른 AuthenticationManager를 부모로 가질 수 있다.
- ProviderManager에 Authentication을 핸들링할 수 있는 Provider가 없었을 경우, 이 Authentication을 가지고 부모를 참조하여 인증을 시도한다.
- 또한 여러 ProviderManager가 한 부모를 가지기도 한다.
- 이를 통해 ProviderManager간 중복되는 Provider는 부모에게 주고, 자신은 특화된 Provider만을 가질 수 있게 된다. (이 사실을 알게되고 난 온몸에 소름이 돋았다. 미친 설계)
어떤가... Spring Security, 그렇게 어렵진 않은 것 같기도 하다.
Spring Security는 어떻게 사용해야 하는지에 대한 정보는 많은데, 실제로 어떻게 동작하는지에 대한 정보가 상대적으로 부족한 것 같다. 그만큼 변화무쌍한 영역이고 어려운 영역이라는 생각이 들었다.
이럴 때는 역시 공식문서인 것 같다. ChatGPT와 Claude의 도움을 받아가며 공식문서를 열심히 읽다보면, 프레임워크 개발자는 천재라는 생각이 계속해서 들고, 좋은 설계에 대한 전반적인 시각도 생기는 것 같다.
다음에는 인가의 중요한 코드를 한번 뜯어보는 것도 나쁘지 않을 것 같다.
(모든 내용은 공식문서에서 따온 것이다.)
이 부분 이해가 잘 안갔는데 좋은 글 감사합니다.