이번에 로그인 관련을 맡게 되었지만 스프링 시큐리티도 JWT도 oauth2도 처음이라 모든것이 처음이라 부족한 부분이 많을 수 있습니다.
(ProviderManager에서 인증에 대해서 알아보다보니 인증 과정에 대해서 알아버렸다..ㅠ)
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;
}
}
oauth2는 여차저차 구현을 잘 했지만 자체 로그인을 구현하는데 있어서 이 클래스로 인증을 하는것을 알게 되었다.
그럼 이 친구에 대해서 알아야 사용하는데 문제 없을 것 같다고 판단하여 알아보는 시간을 가져보겠다.
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();
AuthenticationProvider provider = (AuthenticationProvider)var9.next();
위 코드를 통해서 프로바이더 그니까.. username+password, oauth2, DAO(DB) 중 어떤 프로바이더로 들어왔는지 찾는 코드인듯 하다.
result = provider.authenticate(authentication);
이 코드를 통해서 인증을 시도한다. 그 결과에 따라서 리턴을 하기 위함으로 보인다.
이번 케이스의 경우 username+password 구성으로 되어있기 때문에 UsernamePasswordAuthenticationFilter.class가 provider이다.
그럼 provider.class로 가서 검증에 대해서 찾아보면
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
username = username != null ? username.trim() : "";
String password = this.obtainPassword(request);
password = password != null ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
이런식으로 검증을 하고 있다.
그럼 다시 provider의 result 부분으로 돌아가보자!
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;
}
여기서 result가 null이라는 이야기는 인증이 실패했다는 이야기가 된다. 그러면 break가 되지 못했으니 새로운 프로바이더를 찾으러 갈것이다.
우리는 인증이 성공했다는 기준으로 생각해서 세부 정보에 대해서 복사 후 루프를 빠져나오게 된다.
(copyDetails에서도 사실 올바른지 체크를 하고 있지만 그 부분은 따로 찾아보길 바란다.)
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;
}
마지막으로 여기에 도달하게 되는데 여기서는
(CredentialsContainer)result).eraseCredentials()이 코드가 핵심이다.
지금까지 로그인검증을 위해서 우리는 패스워드에 대해서 검증을 하기 위해 가지고 있었지만, 이미 위에서 올바른 사용자라는 검증을 받은 상태이다.
그렇다면 패스워드에 대해서 가지고 있을 이유가 없기 때문에 UsernamePasswordAuthenticationToken에 있는 credential 부분을 지우게 된다.
try {
Authentication authenticationResult = this.attemptAuthentication(request, response);
if (authenticationResult == null) {
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
this.successfulAuthentication(request, response, chain, authenticationResult);
} catch (InternalAuthenticationServiceException var5) {
InternalAuthenticationServiceException failed = var5;
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
this.unsuccessfulAuthentication(request, response, failed);
} catch (AuthenticationException var6) {
AuthenticationException ex = var6;
this.unsuccessfulAuthentication(request, response, ex);
}
AbstractAuthenticationProcessingFilter 코드를 봤을 때 successfulAuthentication가 작동하게 되어 후 처리를 하게 되는 것이다.
로그인 인증 과정을 요약하자면
1. 스프링 시큐리티의 filter에서 UsernamePasswordAuthenticationFilter.class가 로그인 관련 정보를 가로챈다.
2. AbstractAuthenticationProcessingFilter추상 클래스에서 오버라이드된 attemptAuthentication가 작동한다.
3. AuthenticationManager.authenticate가 검증을 시작한다.
4. ProviderManager에서 Provider를 확인한다. provider에 대해서 검증을 시작한다.
5. 검증을 완료하고 내용에 대해서 세부 정보에 대해서 복사를 한다.
6. result.eraseCredentials()에서 비밀번호 등 민감한 정보를 제거한다.
7. AbstractAuthenticationProcessingFilter에서 result의 값이 null인지 체크하고
8. (skip)세션에 관련된 내용을 체크하지만 jwt를 사용하는 입장에서는 스킵이 된다.
9. null이 아니라면 successfulAuthentication를 동작시킨다.