본문 바로가기
2025 Dev Log/2025.Backend

[#3][다시, 처음부터 #7 심화]

by Dev.후크리 2025. 9. 28.
1. 개념 설명 (Concept)
  • 6일차: In-Memory Authentication (메모리에 계정 저장)
  • 7일차: Database Authentication (DB에 사용자 정보 저장 → UserDetailsService 구현)

흐름:

  1. 클라이언트가 /login 요청 보냄.
  2. AuthenticationManager가 UserDetailsService를 호출.
  3. UserDetailsService는 DB에서 사용자 검색.
  4. 비밀번호 검증(PasswordEncoder) → 성공 시 Authentication 객체 반환.
  5. Spring Security FilterChain이 인증 상태를 SecurityContext에 저장.

2. 올바른 용어 (Terminology)
  • Authentication (인증): 사용자가 누구인지 확인.
  • Authorization (인가): 인증된 사용자가 무엇을 할 수 있는지 확인.
  • UserDetailsService (사용자 정보 서비스): DB에서 사용자 로딩.
  • PasswordEncoder (비밀번호 암호화기): 비밀번호 검증 담당.
  • SecurityContext (보안 컨텍스트): 현재 인증된 사용자 정보를 담는 저장소.

3. 잘못 쓰기 쉬운 표현 + 실제 맥락 (Misuse & Context)
  • ❌ “DB 인증이 JWT다” → JWT는 토큰 기반 인증 방식이고, DB 인증은 그 **전단계(사용자 검증 로직)**임.
  • ❌ “Spring이 알아서 DB에 저장한다” → 실제로는 JPA/Repository에서 직접 User 조회해야 함.

4. 면접 예시 (Interview Q&A)

Q1. Spring Security에서 DB 기반 인증 시 필수로 구현해야 하는 인터페이스는?
A. UserDetailsService 인터페이스이며, loadUserByUsername() 메서드에서 DB 조회를 구현합니다.

Q2. 비밀번호 검증은 어디서 처리되나요?
→ **A. PasswordEncoder**가 사용자 입력 비밀번호와 DB 저장된 해시를 비교합니다.


5. 검증 요약 (Self-Check)
  • 가정: DB(MySQL)에 users 테이블이 있고, username/password가 저장되어 있음.
  • 확인: UserDetailsService가 DB에서 유저 검색하는지, PasswordEncoder를 적용했는지.
  • 리스크: 비밀번호를 평문 저장하면 보안 취약 → 반드시 BCryptPasswordEncoder 사용.

6. 실습 코드 예제 (Java)

User 엔티티

// 사용자 정보를 DB에 저장하는 엔티티
@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 자동 증가 PK
    private Long id;

    @Column(unique = true, nullable = false) // username은 unique해야 함
    private String username;

    @Column(nullable = false) // 반드시 저장해야 함
    private String password;

    @Column(nullable = false)
    private String role; // ROLE_USER, ROLE_ADMIN 등
}
 

UserRepository

// JPA Repository로 DB에서 User 엔티티 조회
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
}

CustomUserDetails

// Spring Security가 이해할 수 있는 UserDetails 구현체
public class CustomUserDetails implements UserDetails {

    private final User user;

    public CustomUserDetails(User user) {
        this.user = user;
    }

    // 권한 반환
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole()));
    }

    // 로그인 아이디
    @Override
    public String getUsername() {
        return user.getUsername();
    }

    // 로그인 비밀번호
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    // 계정 상태 관련 (여기서는 true로 처리)
    @Override public boolean isAccountNonExpired() { return true; }
    @Override public boolean isAccountNonLocked() { return true; }
    @Override public boolean isCredentialsNonExpired() { return true; }
    @Override public boolean isEnabled() { return true; }
}

CustomUserDetailsService

// DB에서 유저 정보 조회하는 서비스
@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 {
        // DB에서 유저 검색
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("사용자 없음: " + username));

        // UserDetails 객체로 변환 후 반환
        return new CustomUserDetails(user);
    }
}

SecurityConfig

 
// Spring Security 설정
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomUserDetailsService customUserDetailsService;

    public SecurityConfig(CustomUserDetailsService customUserDetailsService) {
        this.customUserDetailsService = customUserDetailsService;
    }

    // 비밀번호 암호화기 등록
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // AuthenticationManager 설정
    @Bean
    public AuthenticationManager authManager(HttpSecurity http) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(customUserDetailsService)
                .passwordEncoder(passwordEncoder())
                .and()
                .build();
    }

    // 보안 필터 체인 설정
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // 테스트용: CSRF 끄기
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(login -> login
                .loginPage("/login") // 커스텀 로그인 페이지 가능
                .permitAll()
            )
            .logout(logout -> logout.permitAll());

        return http.build();
    }
}​

7. 추가 학습 과제 (Next Task)

👉 DB 인증을 완전히 이해하려면:

  1. 비밀번호 해싱 실습하기 → BCryptPasswordEncoder().encode("1234") 출력해보고 DB에 저장.
  2. 로그인 시 DB에 저장된 해시랑 입력값 검증되는지 로그로 확인.
  3. 나중에 JWT랑 연결할 때, 지금 만든 AuthenticationManager와 UserDetailsService가 그대로 재활용됨.