이전 글에서는 Spring Security의 기본 설정과 내부 흐름을 학습하며 보안 필터가 요청을 어떻게 가로채고, 인증 과정을 어떻게 처리하는지 살펴봤습니다.
이번 글에서는 한 단계 더 나아가, Spring Security의 “사용자 관리(User Management)”와 “인증(Authentication)” 구조를 직접 커스터마이징합니다.
목표:
- 기본 In-Memory 인증에서 → 데이터베이스 기반 사용자 인증으로 확장
UserDetailsService,UserDetails,Authentication구조의 관계를 이해- 실무에서 가장 자주 쓰이는 Custom UserDetailsService 패턴 완전 정복
Spring Security에서 “인증(Authentication)”은 단순히 로그인 절차가 아닙니다.
이는 사용자 정보(UserDetails) 와 인증 상태(Authentication) 를 분리해서 관리하는 구조적 시스템입니다.
즉,
UserDetails는 "사람"의 정보,
Authentication은 "그 사람이 지금 인증된 상태인가"를 판단하는 객체입니다.
public interface UserDetails {
String getUsername();
String getPassword();
Collection<? extends GrantedAuthority> getAuthorities();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
Spring Security는 로그인 시 이 두 가지를 연결하여 작동합니다:
UserDetailsService → loadUserByUsername() 호출
→
UserDetails로 사용자 정보 조회→
AuthenticationManager가 비밀번호 검증→ 인증 성공 시
SecurityContext에 저장
이 흐름이 이해되면, Spring Security의 절반은 이미 정복한 셈입니다.
Spring Security를 처음 설정하면 보통 이렇게 단순하게 시작합니다:
spring.security.user.name=eazybytes
spring.security.user.password=12345
이 방식은 테스트나 데모용으로는 충분하지만,
애플리케이션이 커지면 다음과 같은 문제가 생깁니다:
따라서 실제 서비스에서는 InMemoryUserDetailsManager를 거쳐 결국 Database 기반 인증으로 발전해야 합니다.
UserDetailsService의 역할과 구조UserDetailsService는 사용자 정보를 로드하는 핵심 인터페이스입니다.
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
Spring Security는 인증 요청이 들어올 때,
AuthenticationManager → AuthenticationProvider → UserDetailsService 순으로 사용자 정보를 조회합니다.
만약 기본 InMemoryUserDetailsManager 대신 DB 기반 사용자 조회를 원한다면, 이 인터페이스를 직접 구현하면 됩니다.
실무에서는 사용자 정보가 DB에 저장되어 있습니다.
따라서 JdbcUserDetailsManager 또는 직접 구현한 UserDetailsService를 사용합니다.
CREATE TABLE customer (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(100) UNIQUE,
password VARCHAR(255),
role VARCHAR(50),
enabled BOOLEAN DEFAULT TRUE
);
Spring Security 기본 구조(users, authorities) 대신
비즈니스에 맞게 필드명을 email, role로 커스터마이징했습니다.
Customer 엔티티와 Repository 구성먼저 DB의 사용자 데이터를 매핑하는 엔티티를 작성합니다.
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
private String password;
private String role;
private boolean enabled;
}
JPA를 이용해 간단히 Repository를 정의합니다.
public interface CustomerRepository extends JpaRepository<Customer, Long> {
Optional<Customer> findByEmail(String email);
}
UserDetailsService 구현이제 핵심 단계입니다.
Spring Security가 로그인 시 호출할 loadUserByUsername() 메서드를
직접 구현하여 DB에서 사용자를 조회하도록 합니다.
@Service
public class EazyBankUserDetailsService implements UserDetailsService {
private final CustomerRepository repository;
public EazyBankUserDetailsService(CustomerRepository repository) {
this.repository = repository;
}
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
Customer customer = repository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return User.builder()
.username(customer.getEmail())
.password(customer.getPassword())
.roles(customer.getRole())
.disabled(!customer.isEnabled())
.build();
}
}
✅ 핵심 포인트
- DB에서 사용자 정보 조회
- Spring Security 표준
User객체로 매핑- 비밀번호는 반드시
PasswordEncoder로 해시 저장
Spring Security 6 이상에서는 평문 비밀번호가 자동으로 거부됩니다.
따라서 반드시 PasswordEncoder Bean을 등록해야 합니다.
@Configuration
public class PasswordConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
비밀번호는 회원가입 시 암호화해서 저장합니다:
customer.setPassword(passwordEncoder.encode(rawPassword));
커스텀 UserDetailsService를 적용한 뒤의 Spring Security 인증 흐름은 다음과 같습니다:
[1] 사용자가 로그인 요청
↓
[2] UsernamePasswordAuthenticationFilter
↓
[3] AuthenticationManager (ProviderManager)
↓
[4] DaoAuthenticationProvider
↓
[5] EazyBankUserDetailsService → DB 조회
↓
[6] PasswordEncoder → 비밀번호 검증
↓
[7] 인증 성공 → SecurityContext 저장
↓
[8] 이후 요청에서 인증 세션 유지 (JSESSIONID)
즉, AuthenticationManager는 실제 인증을 하지 않고,
인증을 "위임"합니다.
실질적인 검증은 Provider → Service → Encoder로 연결됩니다.
로그인 성공 이후, Spring Security는 SecurityContext에 현재 로그인된 사용자의 Authentication 객체를 저장합니다.
컨트롤러나 서비스에서 다음과 같이 확인할 수 있습니다:
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
System.out.println("현재 사용자: " + auth.getName());
System.out.println("권한: " + auth.getAuthorities());
출력 예시:
현재 사용자: eazybytes@example.com
권한: [ROLE_USER]
| 구분 | 핵심 개념 | 비고 |
|---|---|---|
| UserDetailsService | 사용자 정보를 로드하는 인터페이스 | 직접 구현 가능 |
| UserDetails | 사용자 속성(이메일, 비밀번호, 권한 등) | DB 매핑 |
| Authentication | 인증 상태(성공/실패/익명 여부) | 세션 단위 |
| PasswordEncoder | 비밀번호 해싱 및 검증 | bcrypt 기본 |
| SecurityContext | 인증 정보를 세션에 저장 | 자동 유지 |
💡 정리하면,
Spring Security의 핵심은 "사용자 정보(UserDetails)"를 어디서 로드하느냐입니다.
나머지 인증 매커니즘은 그 위에서 자동으로 작동합니다.