일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 프로그래밍
- 개발
- 우리카드
- spring
- 스프링
- 컴퓨터공학
- 디지털
- CS
- github
- 메모리
- OS
- 세마포어
- 공채
- 신입사원
- 스터디
- 신입
- 뮤텍스
- 깃
- 알고리즘
- 자바
- 깃허브
- java
- 이펙티브 자바
- 운영체제
- package-private
- Public
- 컴퓨터과학
- IT
- 정보처리기사
- Effective Java
- Today
- Total
주니어 개발자 성장기
4. Spring Security - 인증 과정 이해하기 (2) 본문
Overview
이번 포스팅에서는AuthenticationManager
의 구현체인 ProviderManager
의 동작과 하위 계층에서 어떤 일이 일어나는 지 확인해보려고 한다.
ProviderManager
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
...
private List<AuthenticationProvider> providers = Collections.emptyList();
...
}
ProviderManager
는 AuthenticationProvider
를 List
에 담은 필드로 가지고 있다. 그러면 인증은 어떻게 이루어 질까? 바로 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
우선 DaoAuthenticationProvider
은 AbstractUserDetailsAuthenticationProvider
를 상속한다. 사실 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);
}
...
}
위 메서드에서 호출되는 additionalAuthenticationChecks
과 retrieveUser
메서드는 subclass에 위임하는 형식으로 되어 있다. 즉, UsernamePasswordAuthenticationFilter
처럼 템플릿 메서드 패턴으로 구현되어 있는 것이다. 먼저 DaoAuthenticationProvider
의 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;
...
}
getUserDetailsService
는 userDetailsService
필드를 반환하는 메서드로서 UserDetailsService
에서 username
으로 loadUserByUsername
을 호출해서 유저 정보를 가져오는 것을 확인할 수 있다.
UserDetailsService
는 인터페이스로서 다양한 구현체가 있다. 따로 설정을 하지 않으면 InMemoryUserDetailsManager
가 디폴트로 설정되어 있다.
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
클래스로 전환했다.UserInfo
는UserDetails
를 구현한 클래스로, 자세한 내용은 깃허브에서 확인바란다.@Service
애노테이션으로 컴포넌트 스캔의 대상이 돼서Bean
으로 등록되면 자동으로InMemoryUserDetailsManager
를 대체해준다. 따라서, 이렇게 하면 별다른 설정이 필요하지 않다.
이렇게 UserDetails를 반환 하는데 성공했다면 그 다음은 비밀번호를 체크하는 additionalAuthenticationChecks
메서드를 확인해 볼 차례이다. 분량상 다음 시간에 알아보자
정리
UsernamePasswordAuthenticationFilter
는ProviderManager
의authenticate
를 호출하는데 여기에는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 |