주니어 개발자 성장기

4. Spring Security - 인증 과정 이해하기 (2) 본문

예제/Session

4. Spring Security - 인증 과정 이해하기 (2)

Junpyo Lee 2023. 6. 15. 07:37

Overview

이번 포스팅에서는AuthenticationManager 의 구현체인 ProviderManager의 동작과 하위 계층에서 어떤 일이 일어나는 지 확인해보려고 한다.

ProviderManager

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

	...

	private List<AuthenticationProvider> providers = Collections.emptyList();

	...
}

ProviderManagerAuthenticationProviderList에 담은 필드로 가지고 있다. 그러면 인증은 어떻게 이루어 질까? 바로 Override한 authenticate를 통해 이루어진다. 자세히 살펴 보자.

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {

	...

	int size = this.providers.size();
	for (AuthenticationProvider provider : getProviders()) {
		if (!provider.supports(toTest)) {
			continue;
		}

		...

		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;
		}

		...	
	}

	...
}

해당 메서드를 보면 각 AuthenticationProvider를 루프를 돌면서 authenticate를 호출하는 것을 확인할 수 있을 것이다.

  • getProviders() 메서드는 단순히 providers 필드를 리턴하는 함수이다.

여기서 등장하는 AuthenticationProvider역시 인터페이스로 여러 구현체가 존재한다. 그 중에서도 일반 로그인에서 쓰이는 것은 DaoAuthenticationProvider이다

ProviderManager.authenticate에 Breakpoint를 잡고 확인해보니 providers 필드에는 오직 DaoAuthenticationProvider만 들어가있다.

그럼 DaoAuthenticationProvider는 내부가 어떻게 구현되어 있을까?

DaoAuthenticationProvider

우선 DaoAuthenticationProviderAbstractUserDetailsAuthenticationProvider를 상속한다. 사실 authentciate 메소드는 여기에 구현되어 있다.

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {

	...

	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");
	}
	try {
		this.preAuthenticationChecks.check(user);
		additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
	}

	...

}

위 메서드에서 호출되는 additionalAuthenticationChecksretrieveUser 메서드는 subclass에 위임하는 형식으로 되어 있다. 즉, UsernamePasswordAuthenticationFilter 처럼 템플릿 메서드 패턴으로 구현되어 있는 것이다. 먼저 DaoAuthenticationProviderretrieveUser 메서드를 살펴보자.

@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;
	...
}

 

getUserDetailsServiceuserDetailsService 필드를 반환하는 메서드로서 UserDetailsService에서 username으로 loadUserByUsername을 호출해서 유저 정보를 가져오는 것을 확인할 수 있다.

UserDetailsService 는 인터페이스로서 다양한 구현체가 있다. 따로 설정을 하지 않으면 InMemoryUserDetailsManager가 디폴트로 설정되어 있다.

ProviderManager의 authenticate 메서드에 Breakpoint를 잡고 확인한 모습

InMemoryUserDetailsManager에는 Username을 키로, MutableUser를 값으로 갖는 Map을 필드로 갖고 있으며 거기서 부터 유저 정보(UserDetails를 가져온다.

여기서 UserDetails란?

유저에 관한 정보(username, password, principal 등)을 가지는 인터페이스로서, 추후에 확인 하겠지만 인증 과정에서 password가 사용된다.

일반적으로 계정은 메모리가 아니라 통합된 DB에 저장하기 때문에 다른 방법을 사용해야 한다. 방법은 간단하다. UserDetailsService를 구현한 클래스를 만들어서 빈으로 등록하면 Spring이 자동적으로 InMemoryUserDetailsManager를 대체해준다. 나는 다음과 같이 AuthenticationService라는 클래스를 만들었다..



@Slf4j
@Service
public class AuthenticationService implements UserDetailsService {

	private final UserRepository userRepository;

	public AuthenticationService(UserRepository userRepository) {
		this.userRepository = userRepository;
	}

	@Override
	public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
		User user = getByEmail(email);
		return UserInfo.of(user, getGrantedAuthority(user.getRole()));
	}

	private List<GrantedAuthority> getGrantedAuthority(Role role) {
		return List.of(new SimpleGrantedAuthority(role.getUserRole().getName()));
	}

	public User getByEmail(String email) {
		return userRepository.findByEmail(email)
			.orElseThrow(() -> new UsernameNotFoundException("해당 이메일의 계정이 없습니다."));
	}
}

 

  • UserRepository에서 email으로 User Entity를 가져온 뒤에 UserInfo 클래스로 전환했다.
  • UserInfoUserDetails를 구현한 클래스로, 자세한 내용은 깃허브에서 확인바란다.
  • @Service 애노테이션으로 컴포넌트 스캔의 대상이 돼서 Bean으로 등록되면 자동으로 InMemoryUserDetailsManager를 대체해준다. 따라서, 이렇게 하면 별다른 설정이 필요하지 않다.

이렇게 UserDetails를 반환 하는데 성공했다면 그 다음은 비밀번호를 체크하는 additionalAuthenticationChecks 메서드를 확인해 볼 차례이다. 분량상 다음 시간에 알아보자

정리

  • UsernamePasswordAuthenticationFilterProviderManagerauthenticate를 호출하는데 여기에는 List<AuthenticationProvider>가 필드로 있으며 다시 루프를 돌면서 authenticate를 호출한다.
  • 폼 로그인의 경우,AuthenticationProvider 가 기본적으로 DaoAuthenticationProvider단 하나만 존재하며 여기서는 UserDetailsService를 통해 UserDetails 객체를 가져온다.
  • 그래서 UserDetails, UserDetailsService를 따로 구현해서 Bean으로 등록하면 개발자가 원하는 방식으로 유저 정보를 가져올 수 있다.

'예제 > Session' 카테고리의 다른 글

3. Spring Security - 인증 과정 이해하기 (1)  (0) 2023.05.27
2. Spring Session  (0) 2023.05.18
1. 설정  (0) 2023.05.16
0. 프로젝트 목적  (0) 2023.05.16