6일차에서 JWT 필터로 “토큰 → 필터 → SecurityContext” 흐름을 이미 실습했으니까,
이번엔 실제 서비스처럼 DB 사용자 계정과 권한(Role)을 기반으로 인증을 붙이는 단계
1. 개념 설명 (Concept)
- 지금까지는 토큰 안에 username을 “직접 적어두고” 그대로 신뢰했지?
- 하지만 실제 서비스에서는 사용자가 DB에 저장돼 있고, 로그인 시 DB에서 비밀번호 검증 → JWT 발급을 거쳐야 해.
흐름
- 클라이언트: /auth/login 요청 (username + password)
- 서버: AuthenticationManager → UserDetailsService 통해 DB에서 사용자 조회
- PasswordEncoder로 비밀번호 검증
- 성공 시 JWT 발급
- 이후 요청은 6일차에서 만든 JWT 필터가 토큰 검증 → SecurityContext 주입
👉 즉, 오늘 배우는 건 UserDetailsService + DB 엔티티 + PasswordEncoder.
2. 올바른 용어 (Terminology)
- UserDetails (사용자 상세 정보): 스프링 시큐리티가 이해할 수 있는 사용자 모델 인터페이스
- UserDetailsService (사용자 조회 서비스): username으로 UserDetails를 DB에서 가져오는 서비스
- PasswordEncoder (비밀번호 인코더): 비밀번호 비교 시 해시 사용 (BCryptPasswordEncoder)
- AuthenticationManager (인증 매니저): 로그인 요청을 인증하는 핵심 매니저
- GrantedAuthority (권한 정보): ROLE_USER, ROLE_ADMIN 같은 역할 문자열
3. 잘못 쓰기 쉬운 표현 + 실제 맥락 (Misuse & Context)
- 오해: “DB에 비밀번호 그냥 저장해도 되지 않나요?”
→ ❌ 절대 금지. 항상 BCryptPasswordEncoder 등 단방향 해시로 암호화 저장. - 오해: “User 엔티티를 바로 UserDetails로 쓰면 되지 않나요?”
→ 가능은 하지만, **UserDetails 인터페이스를 구현한 별도 클래스(UserPrincipal)**를 만들어서 보안용 객체와 엔티티를 분리하는 게 현업에서 선호됨.
4. 면접 예시 (Interview Q&A)
Q1. UserDetails와 User 엔티티를 왜 분리하나요?
A1. User 엔티티는 JPA 비즈니스 객체고, UserDetails는 보안 프레임워크 전용 객체라 책임이 달라서 분리하는 게 유지보수에 유리합니다.
Q2. PasswordEncoder가 꼭 필요한 이유는?
A2. 비밀번호를 평문으로 저장하면 보안 위협이 크므로 반드시 단방향 해시(BCrypt 등)로 저장하고, 로그인 시 해시를 비교해야 합니다.
5. 검증 요약 (Self-Check)
- 가정/전제: H2/MySQL 같은 DB 연결됨. User/Role 테이블 존재.
- 확인 포인트: UserDetailsService → DB 조회, PasswordEncoder 일치 검증, JWT 발급 흐름 연결.
- 리스크/대안: 권한/사용자 테이블 관계(N:1) 잘못 매핑 시 인증 실패.
📌 예시 코드 (Java)
1) User 엔티티
package com.example.security.domain;
import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password; // 암호화 저장
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<RoleEntity> roles = new HashSet<>();
// getter/setter 생략
}
2) Role 엔티티
package com.example.security.domain;
import jakarta.persistence.*;
@Entity
@Table(name = "roles")
public class RoleEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String name; // "ROLE_USER", "ROLE_ADMIN"
// getter/setter 생략
}
3) UserRepository
package com.example.security.repository;
import com.example.security.domain.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<UserEntity, Long> {
Optional<UserEntity> findByUsername(String username);
}
4) UserPrincipal (UserDetails 구현체)
package com.example.security.service;
import com.example.security.domain.UserEntity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.stream.Collectors;
public class UserPrincipal implements UserDetails {
private final UserEntity user;
public UserPrincipal(UserEntity user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toSet());
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return true; }
@Override
public boolean isCredentialsNonExpired() { return true; }
@Override
public boolean isEnabled() { return true; }
}
5) CustomUserDetailsService
package com.example.security.service;
import com.example.security.domain.UserEntity;
import com.example.security.repository.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@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 {
UserEntity user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + username));
return new UserPrincipal(user);
}
}
6) SecurityConfig
package com.example.security.service;
import com.example.security.domain.UserEntity;
import com.example.security.repository.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@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 {
UserEntity user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + username));
return new UserPrincipal(user);
}
}
👉 여기까지 하면 DB에 있는 사용자 + 비밀번호 검증 후 JWT 발급까지 완성.
👉 8일차에서 이제 권한(ROLE)에 따라 접근 제한을 추가할 수 있음.
'2025 Dev Log > 2025.Backend' 카테고리의 다른 글
| [#3][다시, 처음부터 #7 심화] (0) | 2025.09.28 |
|---|---|
| [#3][다시, 처음부터 #6]JWT를 활용한 실전 적용과 필터 기반 인증 흐름 (2) | 2025.09.26 |
| [#3][다시, 처음부터 #5]JWT(Json Web Token) 기반 인증 (0) | 2025.09.23 |
| [#3][다시, 처음부터 #4]서버의 동작 방식과 상태 관리 (0) | 2025.09.17 |
| [#3][다시, 처음부터 #3]HTTP 상태 코드(HTTP Status Code) 와 API 응답 설계 (0) | 2025.09.11 |