Spring Security + JWT로 로그인 구현하기 (2) - DB 연동 및 회원 검증
지난 포스팅에서는 로그인 요청을 가로채는 LoginFilter의 프레임 만들었습니다. 이번에는 AuthenticationManager가 실제로 DB에 저장된 회원 정보와 사용자가 제출한 정보를 비교하여 인증을 수행하도록 하는 내부 로직을 구현해 보겠습니다.
이 과정에서는 Spring Security의 핵심 인터페이스인 UserDetailsService와 UserDetails를 우리 시스템에 맞게 커스텀해보겠습니다.
- UserDetailsService: username을 받아 DB에서 사용자 정보를 조회하는 회원 탐색 역할
- UserDetails: 조회된 사용자 정보를 Spring Security가 이해할 수 있는 형태로 감싸주는 시큐리티용 회원 증명서 역할
1단계: UserRepository에 회원 조회 메소드 추가
가장 먼저, 데이터베이스에서 username을 기준으로 회원을 조회할 수 있는 기능이 필요합니다. Spring Data JPA를 사용하므로, 정해진 규칙에 따라 메소드를 선언하기만 하면 됩니다.
UserRepository.java
public interface UserRepository extends JpaRepository<UserEntity, Integer> {
// username을 받아 DB 테이블에서 회원을 조회하는 메소드
UserEntity findByUsername(String username);
Boolean existsByUsername(String username);
}
2단계: UserDetailsService 커스텀 구현
이제 AuthenticationManager가 인증을 처리할 때 호출할 UserDetailsService를 직접 구현할 차례입니다.
CustomUserDetailsService.java
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. DB에서 username으로 회원 조회
UserEntity userData = userRepository.findByUsername(username);
if (userData != null) {
// 2. 조회된 회원 정보를 CustomUserDetails 객체로 감싸서 반환
// 이 객체를 반환하면 AuthenticationManager가 비밀번호 검증을 수행함
return new CustomUserDetails(userData);
}
// 조회된 회원이 없을 경우 null 또는 예외 처리
return null;
}
}
AuthenticationManager는 loadUserByUsername(username)을 호출하여 DB로부터 회원 정보를 가져오도록 요청합니다. UserRepository를 통해 회원을 조회한 뒤에 다음 단계에서 만들 CustomUserDetails 객체로 감싸서 반환하면 됩니다.
3단계: UserDetails 커스텀 구현
UserDetails는 Spring Security가 사용자의 인증 정보를 인식하는 표준 형식입니다. 우리의 회원 정보 객체(UserEntity)를 UserDetails 형식으로 변환해 주는 어댑터 클래스를 만듭니다.
CustomUserDetails.java
public class CustomUserDetails implements UserDetails {
private final UserEntity userEntity;
public CustomUserDetails(UserEntity userEntity) {
this.userEntity = userEntity;
}
// 사용자의 권한(Role)을 반환
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return userEntity.getRole();
}
});
return collection;
}
// 사용자의 비밀번호를 반환
@Override
public String getPassword() {
return userEntity.getPassword();
}
// 사용자의 아이디(username)를 반환
@Override
public String getUsername() {
return userEntity.getUsername();
}
// 아래 4개 메소드는 계정의 상태를 관리
@Override
public boolean isAccountNonExpired() {
return true; // 계정 만료 여부
}
@Override
public boolean isAccountNonLocked() {
return true; // 계정 잠금 여부
}
@Override
public boolean isCredentialsNonExpired() {
return true; // 비밀번호 만료 여부
}
@Override
public boolean isEnabled() {
return true; // 계정 활성화 여부
}
}
이제 전체 로그인 흐름을 다시 정리해보자면
전체 로그인 검증 흐름
- 사용자가 ID/PW로 /login 요청을 보냅니다.
- LoginFilter가 요청을 가로채 UsernamePasswordAuthenticationToken을 생성합니다.
- 생성된 Token을 AuthenticationManager에게 전달하여 인증을 위임합니다.
- AuthenticationManager는 우리가 등록한 CustomUserDetailsService의 loadUserByUsername 메소드를 호출합니다.
- CustomUserDetailsService는 UserRepository를 통해 DB에서 회원을 조회하고, CustomUserDetails로 감싸서 반환합니다.
- AuthenticationManager는 CustomUserDetails에 담긴 비밀번호와 사용자가 요청한 비밀번호가 일치하는지 검증합니다.
- 검증에 성공하면, LoginFilter의 successfulAuthentication() 메소드가 호출됩니다.
이제 DB를 통한 인증까지 구현되었습니다.
'백엔드 스터디' 카테고리의 다른 글
| Aiven이란 (0) | 2025.08.24 |
|---|---|
| Swagger로 API 자동 관리하기 (1) | 2025.08.24 |
| 스프링 OAuth2 로그인 구현하기 3 - JWT (1) | 2025.08.17 |
| 스프링 OAuth2 로그인 구현하기 2 (4) | 2025.08.09 |
| 스프링 OAuth2 로그인 구현하기 1 (2) | 2025.08.04 |