Spring Security - AuthenticationProvider

목차

참고

Spring Security - AuthenticationProvider

AuthenticationProvider.java

AuthenticationManager로 부터 인증을 위임받아 실제로 인증을 진행하는 객체, AuthenticationProvider의 구현체로는 AbstractUserDetailsAuthenticationProviderDaoAuthenticationProvider 가 존재한다.

AuthenticationProvider 의 인증 과정

  • 전달받는 authentication 에서 username을 가져온 뒤 DB에서 해당 하는 유저 정보를 가져온다.
  • 인증을 진행하기 전 DB로부터 가져온 UserDetails 객체에 대한 유효성 검사를 진행한다.
  • 전달 받은 Authentication 객체내의 Credential 정보와 UserDetails 객체의 Password와 값이 일치하는지 확인한다.
    • username에 대한하는 UserDetails 객체를 못 찾을 경우 UsernameNotFoundException 예외를 던지고
    • Credentials가 Password와 다를 경우 BadCredentialsException 예외를 던진다.
  • 인증 후 다시한번 UserDetails 객체의 유효성 검사를 진행하고, 캐시에 UserDetails 객체를 저장한다.
  • 새로운 Authentication 객체를 만들어 반환한다.
public interface AuthenticationProvider {	
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}

UserDetails를 이용해 검증하는 AbstractUserDetailsAuthenticationProvider

AbstractUserDetailsAuthenticationProvider 클래스는 AuthenticationProvider를 구현한 클래스다. AbstractUserDetailsAuthenticationProvider 클래스는 전달 받은 Authentication 객체와 기존에 저장된 UserDetails 객체를 이용해 인증을 진행하는 로직이 구현 돼 있다.

AbstractUserDetailsAuthenticationProvider.java

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));

String username = determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);

if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}

// UserDetails 객체에 대한 상태를 체크한다.
try {
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
}

cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}

this.postAuthenticationChecks.check(user);

if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}

Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}

인증 후 새로운 인증 객체 반환

AbstractUserDetailsAuthenticationProvider.java

protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
this.logger.debug("Authenticated user");
return result;
}

DB에 저장된 User 정보를 가져오기 위한 DaoAuthenticationProvider

DaoAuthenticationProvider 클래스는 AbstractUserDetailsAuthenticationProvider 를 상속해 구현한 클래스다. DaoAuthenticationProvider 클래스에는 주로 DB로부터 User 정보를 조회 및 가져온 User 정보를 검증하는 로직이 구현 돼 있다.

DB에 저장된 User 정보 가져오기

DaoAuthenticationProvider.java

@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
// username을 이용해 사용자 정보를 가져온다.
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
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);
}
}

Authentication 객체의 Credentials 정보와 UserDetails 객체의 비밀번호가 일치하는지 검증

DaoAuthenticationProvider.java

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())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}

UserDetailsChecker 객체를 이용한 UserDetails 객체에 대한 검증

UserDetailsChecker 객체는 DB로부터 혹은 캐시에 저장된 UserDetails 객체에 대한 유효성을 검증한다.

DefaultPreAuthenticationChecks.java

private class DefaultPreAuthenticationChecks implements UserDetailsChecker {

@Override
// 계정이 잠겼는지, 사용가능한지, 만료됐는지 확인한다.
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account is locked");
throw new LockedException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
}
if (!user.isEnabled()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account is disabled");
throw new DisabledException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
}
if (!user.isAccountNonExpired()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account has expired");
throw new AccountExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
}
}
}

인증 전 Authentication 객체

인증 전 Authenticatin 객체내의 principal에는 username이 credentials는 password가 들어있고, 인증 유무를 나타내는 authenticated 값이 false로 나타나 있다.

인증 후 Authentication 객체

인증을 진행한 후 인증 전과 다르게 Authenticatin 객체의 principal에는 UserDeails 를 구현한 객체가 들어갔고 authenticated 값이 true가 됐다. 최종적으로 반환 될 때는 Authenticatin 객체에서 credentials를 지우고 반환한다.

Share