
💡 로그인 로직
- 사용자가 로그인 폼에 인증 정보를 입력한 후 로그인을 시도하였을 때, UsernamePasswordAuthenticationFilter가 이를 가로채서 AuthenticationManager에게 전달한다.
- 이때, UsernamePasswordAuthenticationToken을 사용하여 Authentication 객체를 생성한다.
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
- AuthenticationManager는 JwtAuthenticationProvider에서 UserDetailsService의 loadUserByUsername를 사용하여 입력된 이름을 이용하여 UserDetails를 가져온다. 이후 입력된 비밀번호와 저장된 비밀번호를 비교하여 인증을 수행한다.
- 성공하면, UsernamePasswordAuthenticationToken을 사용하여 Authentication 객체를 생성한다.
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (passwordEncoder.matches(password, userDetails.getPassword())) {
Authentication authenticationResult = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationResult);
}
- 해당 Authentication 객체는 SecurityContextHolder의 SecurityContext에 저장되어 스레드 로컬에 보관된다. 이렇게 함으로써 인증된 사용자에 대한 정보를 어디서든지 쉽게 얻을 수 있도록 한다.
💡 UserDetailsService
- username 정보는 여러가지가 있을 수 있으므로 커스텀한 메서드를 만들어야 한다.
- 앞서 설명했듯이 이를 처리하는 부분은 UserDetailsService의 loadUserByUsername 메서드이다. 이 메서드를 오버라이드하여 아이디 또는 이메일로 사용자를 찾는 기능을 추가할 수 있다.
@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UserNotFoundException(SecurityExceptionCode.USER_NOT_FOUND));
return createUserDetails(user);
}
public UserDetails createUserDetails(User user) {
return PrincipalDetails.builder()
.username(String.valueOf(user.getId()))
.password(user.getPassword())
.authority(user.getAuthority().toString())
.build();
}
}
💡 UserDetails
- Spring Security에서 제공하는 기본 UserDetails 구현체는 사용자의 아이디, 비밀번호, 권한 정보만을 다룬다.
- 실제 애플리케이션에서 다양한 정보가 필요한 경우 유연하게 추가할 수 있다.
@Getter
public class PrincipalDetails implements UserDetails {
private final String username;
private final String password;
private final String authority;
private final Collection<GrantedAuthority> authorities;
@Builder
private PrincipalDetails(
String username, String password, String authority, Collection<GrantedAuthority> authorities
) {
this.username = username;
this.password = password;
this.authority = authority;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton((GrantedAuthority) () -> authority);
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
✋ 여기서 잠깐
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.formLogin()
.disable()
return http.build();
}
private Authentication authenticateUser(LoginRequest loginRequest) {
UsernamePasswordAuthenticationToken authenticationToken = loginRequest.toAuthentication();
return authenticationManagerBuilder.getObject().authenticate(authenticationToken);
}
- 로그인폼 비활성화 시 AnonymousAuthenticationFilter가 실행되어 SecurityContextHolder를 익명 사용자로 설정한다.
- AuthenticationManagerBuilder에서 AuthenticationManager를 얻고 AuthenticationManager를 사용하여 사용자를 실제로 인증한다. 인증이 성공하면 해당 사용자의 Authentication 객체를 반환한다.
- 결과적으로, authenticateUser 메서드를 호출하면 해당 메서드 내에서 사용자의 인증이 진행되고, SecurityContextHolder에는 해당 사용자의 정보가 설정된다.