본문으로 건너뛰기
Advertisement

11.2 BCrypt 암호화 및 UserDetailsService 구현

회원 정보가 데이터베이스에 저장될 때 비밀번호는 절대 평문으로 저장되어서는 안 됩니다. 또한, 스프링 시큐리티가 DB에 있는 사용자 정보를 읽어올 수 있도록 다리를 놓아주어야 합니다.

1. PasswordEncoder와 BCrypt

BCrypt는 비밀번호 해싱에 널리 사용되는 알고리즘으로, 강력한 보안성과 함께 솔트(Salt) 를 자동으로 생성하여 레인보우 테이블 공격을 방어합니다.

PasswordEncoder 빈 등록

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

2. UserDetails와 UserDetailsService

스프링 시큐리티가 사용자 정보를 이해할 수 있도록 인터페이스를 구현해야 합니다.

  • UserDetails: 사용자의 이름, 비밀번호, 권한 등의 정보를 담는 객체입니다.
  • UserDetailsService: 데이터베이스 등 저장소에서 사용자 정보를 조회하여 UserDetails를 반환하는 서비스입니다.

UserDetailsService 커스텀 구현 예시

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

private final UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByEmail(username)
.map(user -> User.builder()
.username(user.getEmail())
.password(user.getPassword()) // DB에는 BCrypt로 암호화된 값 필수
.roles(user.getRole().name())
.build())
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + username));
}
}

3. 회원가입 시 비밀번호 암호화

사용자 등록 로직에서 반드시 PasswordEncoder를 사용해야 합니다.

public void signup(UserDto dto) {
String encodedPassword = passwordEncoder.encode(dto.getPassword());
User user = User.builder()
.email(dto.getEmail())
.password(encodedPassword)
.role(Role.USER)
.build();
userRepository.save(user);
}

🎯 핵심 요점

  • 비밀번호는 반드시 BCrypt 와 같은 단방향 해시 알고리즘으로 암호화해야 합니다.
  • UserDetailsService 는 시큐리티와 DB 사이의 연결 고리 역할을 합니다.
  • loadUserByUsername 메서드에서 사용자 존재 여부를 검증하고 UserDetails 객체를 반환합니다.

11.4 인증 처리 흐름과 실전 예제

스프링 시큐리티의 인증 과정은 여러 컴포넌트가 협력하여 이루어집니다. 여기서는 가장 일반적인 폼 로그인(ID/PW) 방식의 전체 라이프사이클을 추적하며 상세히 알아봅니다.

1. 전체 인증 프로세스 (Mermaid)

sequenceDiagram
participant User as 사용자(클라이언트)
participant Filter as AuthenticationFilter
participant Manager as AuthenticationManager
participant Provider as AuthenticationProvider
participant Service as UserDetailsService
participant DB as Database

User->>Filter: 1. 로그인 요청 (username, password)
Filter->>Filter: 2. UsernamePasswordAuthenticationToken 생성 (미인증 상태)
Filter->>Manager: 3. authenticate(token) 호출
Manager->>Provider: 4. 지원하는 Provider에게 인증 위임
Provider->>Service: 5. loadUserByUsername(username)
Service->>DB: 6. 사용자 정보 조회
DB-->>Service: 사용자 엔티티 반환
Service-->>Provider: 7. UserDetails 객체 반환
Provider->>Provider: 8. 비밀번호 검증 (BCrypt 비교)
Provider-->>Manager: 9. 인증된 Authentication 객체 반환
Manager-->>Filter: 10. 인증 완료
Filter->>Filter: 11. SecurityContextHolder에 인증 정보 저장
Filter-->>User: 12. 로그인 성공 응발/리다이렉트

2. 핵심 컴포넌트 역할

  1. AuthenticationFilter: HTTP 요청을 낚아채서 Authentication 객체(미인증)를 만들고 Manager에게 전달합니다.
  2. AuthenticationManager: 실제 인증을 총괄하는 인터페이스입니다. (일반적으로 ProviderManager 구현체 사용)
  3. AuthenticationProvider: 실제 비즈니스 로직(ID/PW 검증 등)을 수행합니다. 여러 개를 등록하여 다중 인증(LDAP, DB, Social 등)을 처리할 수 있습니다.
  4. UserDetailsService: DB에서 사용자 정보를 가져오는 "데이터 제공자" 역할만 수행합니다.
  5. SecurityContextHolder: 인증이 완료된 정보를 애플리케이션 전역에서 참조할 수 있도록 보관하는 저장소입니다.

3. 실전 예제: 로그인 API 구현 (JWT 방식)

최근 추세에 맞춰, 인증 완료 후 JWT 토큰을 발급하는 간단한 컨트롤러 예시입니다.

LoginRequest DTO

public record LoginRequest(String email, String password) {}

AuthController

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

private final AuthenticationManager authenticationManager;
private final TokenProvider tokenProvider;

@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody LoginRequest request) {
// 1. 미인증 토큰 생성
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(request.email(), request.password());

// 2. 실제 인증 시작 (Manager -> Provider -> Service 흐름 작동)
// 인증 실패 시 Exception 발생
Authentication authentication = authenticationManager.authenticate(authenticationToken);

// 3. 인증 정보를 Context에 저장 (생략 가능하나 권장)
SecurityContextHolder.getContext().setAuthentication(authentication);

// 4. 인증된 정보를 바탕으로 JWT 토큰 생성 및 반환
String jwt = tokenProvider.createToken(authentication);
return ResponseEntity.ok(jwt);
}
}

4. 인증 정보 활용 (@AuthenticationPrincipal)

로그인한 사용자의 정보를 컨트롤러에서 바로 꺼내 쓰고 싶을 때 사용합니다.

@GetMapping("/api/me")
public ResponseEntity<String> getMyInfo(@AuthenticationPrincipal UserDetails userDetails) {
return ResponseEntity.ok("현재 접속 유저: " + userDetails.getUsername());
}

🎯 핵심 요점

  • 인증은 Filter -> Manager -> Provider -> Service 단계를 거칩니다.
  • AuthenticationManager 는 인증의 중심 역할을 하며, 실제 검증은 Provider 가 담당합니다.
  • 인증이 성공하면 유저 정보는 SecurityContextHolder 에 담겨 전역에서 접근 가능해집니다.
Advertisement