⛑ 사용자 관리와 인증 구조 (Authentication)

okorion·2025년 10월 6일

🔐 Spring Security 6

목록 보기
2/10

📘 들어가며

이전 글에서는 Spring Security의 기본 설정과 내부 흐름을 학습하며 보안 필터가 요청을 어떻게 가로채고, 인증 과정을 어떻게 처리하는지 살펴봤습니다.

이번 글에서는 한 단계 더 나아가, Spring Security의 “사용자 관리(User Management)”와 “인증(Authentication)” 구조를 직접 커스터마이징합니다.

목표:

  • 기본 In-Memory 인증에서 → 데이터베이스 기반 사용자 인증으로 확장
  • UserDetailsService, UserDetails, Authentication 구조의 관계를 이해
  • 실무에서 가장 자주 쓰이는 Custom UserDetailsService 패턴 완전 정복

#1. 인증(Authentication)과 사용자(User)의 분리 개념

Spring Security에서 “인증(Authentication)”은 단순히 로그인 절차가 아닙니다.

이는 사용자 정보(UserDetails)인증 상태(Authentication) 를 분리해서 관리하는 구조적 시스템입니다.

  • UserDetails: 사용자의 기본 정보(아이디, 비밀번호, 권한 등)를 보관
  • Authentication: 로그인 성공 여부, 인증 방식(FormLogin, Token 등), 인증 결과를 포함

즉,

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의 절반은 이미 정복한 셈입니다.


#2. 기본 In-Memory 인증의 한계

Spring Security를 처음 설정하면 보통 이렇게 단순하게 시작합니다:

spring.security.user.name=eazybytes
spring.security.user.password=12345

이 방식은 테스트나 데모용으로는 충분하지만,

애플리케이션이 커지면 다음과 같은 문제가 생깁니다:

  • 계정을 동적으로 추가하거나 수정할 수 없다
  • DB나 외부 시스템 연동이 불가능하다
  • 비밀번호가 평문으로 저장된다

따라서 실제 서비스에서는 InMemoryUserDetailsManager를 거쳐 결국 Database 기반 인증으로 발전해야 합니다.


#3. UserDetailsService의 역할과 구조

UserDetailsService는 사용자 정보를 로드하는 핵심 인터페이스입니다.

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

Spring Security는 인증 요청이 들어올 때,

AuthenticationManagerAuthenticationProviderUserDetailsService 순으로 사용자 정보를 조회합니다.

만약 기본 InMemoryUserDetailsManager 대신 DB 기반 사용자 조회를 원한다면, 이 인터페이스를 직접 구현하면 됩니다.


#4. DB 기반 사용자 관리로 전환하기

실무에서는 사용자 정보가 DB에 저장되어 있습니다.

따라서 JdbcUserDetailsManager 또는 직접 구현한 UserDetailsService를 사용합니다.

🗂️ DB 테이블 설계 예시

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로 커스터마이징했습니다.


#5. 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);
}

#6. 커스텀 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로 해시 저장

#7. 비밀번호 암호화 처리

Spring Security 6 이상에서는 평문 비밀번호가 자동으로 거부됩니다.

따라서 반드시 PasswordEncoder Bean을 등록해야 합니다.

@Configuration
public class PasswordConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

비밀번호는 회원가입 시 암호화해서 저장합니다:

customer.setPassword(passwordEncoder.encode(rawPassword));

#8. 인증 흐름 전체 요약

커스텀 UserDetailsService를 적용한 뒤의 Spring Security 인증 흐름은 다음과 같습니다:

[1] 사용자가 로그인 요청
      ↓
[2] UsernamePasswordAuthenticationFilter
      ↓
[3] AuthenticationManager (ProviderManager)
      ↓
[4] DaoAuthenticationProvider
      ↓
[5] EazyBankUserDetailsService → DB 조회
      ↓
[6] PasswordEncoder → 비밀번호 검증
      ↓
[7] 인증 성공 → SecurityContext 저장
      ↓
[8] 이후 요청에서 인증 세션 유지 (JSESSIONID)

즉, AuthenticationManager는 실제 인증을 하지 않고,
인증을 "위임"합니다.
실질적인 검증은 Provider → Service → Encoder로 연결됩니다.


#9. 인증 객체의 상태 확인하기

로그인 성공 이후, Spring Security는 SecurityContext에 현재 로그인된 사용자의 Authentication 객체를 저장합니다.

컨트롤러나 서비스에서 다음과 같이 확인할 수 있습니다:

Authentication auth = SecurityContextHolder.getContext().getAuthentication();
System.out.println("현재 사용자: " + auth.getName());
System.out.println("권한: " + auth.getAuthorities());

출력 예시:

현재 사용자: eazybytes@example.com
권한: [ROLE_USER]

#10. 학습 요약

구분핵심 개념비고
UserDetailsService사용자 정보를 로드하는 인터페이스직접 구현 가능
UserDetails사용자 속성(이메일, 비밀번호, 권한 등)DB 매핑
Authentication인증 상태(성공/실패/익명 여부)세션 단위
PasswordEncoder비밀번호 해싱 및 검증bcrypt 기본
SecurityContext인증 정보를 세션에 저장자동 유지

💡 정리하면,

Spring Security의 핵심은 "사용자 정보(UserDetails)"를 어디서 로드하느냐입니다.

나머지 인증 매커니즘은 그 위에서 자동으로 작동합니다.

profile
Tech Blog

0개의 댓글