본문 바로가기
Spring Security

[Spring Security] UserDetailsService와 UserDetails 및 Authentication의 차이점

by 개발자 JACOB 2023. 6. 10.

1. UserDetailsManager 이해하기


스프링 시큐리티 필터를 거친 다음, AuthenticationManager는 적절한 Provider를 선택한 후 UserDetailsManager를 호출합니다.

상속도

전반적인 상속도입니다. 최하단의 3개의 클래스는 Spring Security에서 기본적으로 제공하는 매니저이며, 개발자가 정의한 매니저를 사용할 수 있습니다.

1. UserDetailsService

UserDetailService에서는 클라이언트에게 받은 username을 검색합니다. 해당 인터페이스에는 loadUserByUsername() 메소드만 정의되어 있습니다.

public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

문뜩 드는 의문은 '왜 username과 password로 검색하지 않는가?' 입니다. 하지만 비밀번호와 관련된 것은 PasswordEncoder가 처리하고, 비밀번호를 확인하는 로직은 뒤에서 처리할 수 있기 때문에 일단 username으로 검색하는 것입니다. 이때 검색한 username으로 UserDetail을 반환합니다.

2. UserDetailsManager

UserDatailsManager는 UserDetailsService를 상속 받은 인터페이스입니다. 해당 인터페이스는 User의 생성, 업데이트, 삭제 등등 기능을 제공합니다.

public interface UserDetailsManager extends UserDetailsService {

	void createUser(UserDetails user);

	void updateUser(UserDetails user);

	void deleteUser(String username);

	void changePassword(String oldPassword, String newPassword);

	boolean userExists(String username);
}

3. InMemoryUserDetailsManager

InMemoryUserDetailsManager는 UserDetailsManager를 상속 받은 클래스로, 스프링 시큐리티가 기본적으로 제공하는 클래스 중 하나입니다. 간단히 구현된 코드를 보겠습니다.

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		UserDetails user = this.users.get(username.toLowerCase());
		if (user == null) {
			throw new UsernameNotFoundException(username);
		}
		return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(),
				user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
	}
public class User implements UserDetails, CredentialsContainer {
	...
}

username으로 검색한 뒤에 User를 반환하고 있습니다. User는 UserDetails을 구현한 클래스이기 때문에 반환될 수 있습니다. 디버깅 모드로 한번 loadUserByUsername을 찍어봤습니다.

젤 하단에서 doFilter 메소드들이 찍힙니다. 이는 맨 위의 그림에서 1번에 해당하는 과정임을 알 수 있습니다. 필터를 거친 다음, ProviderManager가 authenticate()를 하는 모습이 보입니다. ProviderManager는 AuthenticationManager의 구현체입니다. 그러다가 4번의 과정에서 AbstractUserDetailsAuthenticationProvider -> DaoAuthenticationProvider가 선택되었고 5번의 과정으로 InMemoryUserDetailsManager의 loadUserByUsername을 호출하였습니다.

(물론 스프링빈에 InMemoryUserDetailsManager를 등록했습니다.)

2. UserDetails와 Authentication의 차이점


이전 스프링 시큐리티란? 포스팅에서 SecurityContext에는 Authentication이 저장된다고 했습니다. UserDetailsManager과 UserDetailsService의 UserDetails와 Authentication의 차이점은 무엇일까요? 코드로 확인해봅시다.

스프링 시큐리티 필터들을 거친 이후 유저 정보를 추출하여 Authentication을 구현한 토큰을 생성합니다. 그 다음  AuthenticationManager는 AutheticationProvider들을 authenticate()로 호출합니다.

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    ...
    String username = determineUsername(authentication);
    boolean cacheWasUsed = true;
    UserDetails user = this.userCache.getUserFromCache(username);
    if (user == null) {
        ...
        try {
            user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (UsernameNotFoundException ex) {
            ...
        }
        ...
    }
	...
    return createSuccessAuthentication(principalToReturn, authentication, user);
}

그 다음 try-catch 문에서 retrieveUser를 호출하는데,

@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    prepareTimingAttackProtection();
    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) {
        ...
    }
    catch (InternalAuthenticationServiceException ex) {
        ...
    }
    catch (Exception ex) {
        ...
    }
}

retrieveUser 메소드에서는 loadUserByUsername 메소드를 호출합니다. 이 메소드는 위에서 언급했으므로 생략하겠습니다. 아무튼, loadUserByUsername 메소드로 가져온 UserDetails loadUser를 return합니다.

그럼 retrieveUser를 호출한 authenticate 메소드에서는 반환된 UserDtails 객체를 createSuccessAuthentication 메소드의 파라미터로 넣어 리턴합니다.

protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
        UserDetails user) {
    // Ensure we return the original credentials the user supplied,
    // so subsequent attempts are successful even with encoded passwords.
    // Also ensure we return the original getDetails(), so that future
    // authentication events after cache expiry contain the details
    UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(principal,
            authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
    result.setDetails(authentication.getDetails());
    this.logger.debug("Authenticated user");
    return result;
}

createSuccessAuthentication 메소드는 Authentication, 즉 인증 객체를 반환합니다.

3. 총 정리


  1. UserDetailsService/Manager
    • UserDetailsService 인터페이스는 가장 일반적으로 사용될 수 있도록 구축되어 있으며, 스펙은 loadUserByUsername 메소드로, 사용자의 특정 데이터를 로드하는 역할입니다. 여기서 비밀번호와 같이 검색하지 않는데, 비밀번호와 함께 탐색하면 SQL 로그나 네트워크를 통해 비밀번호는 매우 자주 이동될 것이며 이는 이상적인 접근법이 아니기 때문에 이름으로만 검색합니다.
    • UserDetailsManager 인터페이스는 UserDetailsService 인터페이스를 상속 받은 인터페이스입니다. 이름 그대로 UserDetails를 생성, 업데이트, 삭제와 같이 관리하는 역할의 인터페이스입니다.
    • 데이터베이스나 인메모리 저장 공간에서 유저의 정보를 로드하며 관리하는 역할입니다. 또 UserDetails 타입 객체를 Provider에게 넘겨줍니다.
  2. Authentication 타입의 객체와 UserDetails 타입의 객체의 차이점
    • Authentication 타입의 객체는 스프링 시큐리티 전반에 사용되는 객체이며, 인증의 성공 여부를 확인하는 역할이다. SecurityContext에 Authentication이 저장됩니다.
    • UserDetails 타입의 객체는 UserDetailsService와 UserDetailsManager에서 사용되는 객체이며, 데이터베이스나 해쉬맵과 같은 인메모리 저장 공간에서 유저의 정보를 로드할 때 사용됩니다.
      UserDetailsManager는 자신을 호출한 Provider에게 UserDetails를 넘겨주며, Provider는 별도의 메소드를 사용해서(createSuccessAuthentication 메소드와 같은) Authentication을 넘겨줍니다. 즉, 아이디 비밀번호와 같은 유저의 정보는 리턴되지 않는 것입니다.