UserDetailsService를 사용하지 않고, 직접 AuthenticationProvider를 구현하는 것은 특정 상황에서 유용할 수 있습니다. 예를 들어, 사용자 인증 로직이 매우 특화된 경우나, 기존 UserDetailsService를 사용하는 방식이 맞지 않는 경우에 이러한 접근 방식을 사용할 수 있습니다. 그러나, Spring Security가 제공하는 기본 구조와 원칙을 따르는 것이 일반적으로 더 나은 선택일 수 있습니다. 이를 통해 Spring Security의 강력한 기능과 확장성을 활용할 수 있기 때문입니다.
package com.example.loans_domain.config;
import com.example.loans_domain.model.Customer;
import com.example.loans_domain.repository.CustomRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
@RequiredArgsConstructor
public class UsernamePwdAuthenticationProvider implements AuthenticationProvider {
private final CustomRepository customRepository;
private final PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
List<Customer> customer = customRepository.findByEmail(username);
if (customer.size() > 0) {
if (passwordEncoder.matches(password, customer.get(0).getPwd())) {
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(customer.get(0).getRole()));
return new UsernamePasswordAuthenticationToken(customer.get(0).getEmail(), customer.get(0), authorities);
} else {
throw new BadCredentialsException("Invalid password");
}
} else {
throw new BadCredentialsException("No user registered with this details!");
}
}
@Override
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
}
UsernamePasswordAuthenticationToken을 생성하여 인증된 사용자 정보를 SecurityContextHolder에 저장하기 위해 반환합니다. 이 과정에서 사용자 인증이 성공하면 UsernamePasswordAuthenticationToken 객체가 생성되고, 이 객체는 이후의 보안 컨텍스트에서 인증된 사용자로 인식됩니다.
UsernamePwdAuthenticationProvider를 직접 구현하는 것의 장단점
장점:
- 유연성: 사용자 인증 로직을 더 세밀하게 제어할 수 있습니다.
- 특화된 요구사항 처리: 표준 UserDetailsService로 처리할 수 없는 특화된 인증 요구사항을 처리할 수 있습니다.
- 외부 시스템 연동: 외부 시스템이나 API와의 연동을 더 쉽게 처리할 수 있습니다.
단점:
- 복잡성 증가: 직접 인증 로직을 구현하면 코드 복잡성이 증가할 수 있습니다.
- 유지보수 어려움: 커스텀 로직이 많아질수록 유지보수가 어려워질 수 있습니다.
- Spring Security와의 호환성: Spring Security의 기본적인 패턴을 따르지 않으면, 다른 보안 기능들과의 호환성이 떨어질 수 있습니다.
UserDetailsService를 사용하는 방식
Spring Security는 UserDetailsService 인터페이스를 사용하여 사용자 인증 정보를 로드합니다. 이 방식은 Spring Security의 표준 패턴을 따르며, 다양한 인증 방법과 쉽게 통합할 수 있습니다.
장점:
- 일관성: Spring Security의 표준 패턴을 따르므로 다른 보안 기능들과의 호환성이 높습니다.
- 간단한 구현: 많은 경우 UserDetailsService를 구현하는 것이 더 간단합니다.
- 확장성: 다양한 인증 방법과 쉽게 통합할 수 있습니다 (예: OAuth2, JWT 등).
단점:
- 유연성 부족: 매우 특화된 요구사항을 처리하기 위해서는 한계가 있을 수 있습니다.
추천 방법
일반적인 경우라면 UserDetailsService를 구현하고, 이를 통해 사용자 인증을 처리하는 것이 좋습니다. 이렇게 하면 Spring Security의 강력한 기능을 최대한 활용할 수 있습니다.
그러나, 매우 특화된 요구사항이나 외부 시스템 연동이 필요한 경우에는 AuthenticationProvider를 직접 구현하는 것도 고려할 수 있습니다. 이 경우에도 가능하다면 UserDetailsService를 사용하는 구조를 최대한 따르는 것이 좋습니다.
UsernamePwdAuthenticationProvider를 만들지 않으면 Spring Security의 기본 동작 방식인 DaoAuthenticationProvider가 사용됩니다. 이 경우, UserDetailsService를 통해 사용자 정보를 조회하고, 조회된 UserDetails 객체를 사용하여 인증을 진행합니다. 전체적인 과정은 다음과 같습니다:
기본 동작 방식
- 인증 요청 처리:
- UsernamePasswordAuthenticationFilter에서 attemptAuthentication 메서드를 통해 사용자 입력 (username, password)을 받아 UsernamePasswordAuthenticationToken을 생성하고, 이를 AuthenticationManager에 전달합니다.
- AuthenticationManager:
- AuthenticationManager는 등록된 여러 AuthenticationProvider 중 적합한 AuthenticationProvider를 찾아 authenticate 메서드를 호출합니다.
- 기본적으로 Spring Security는 DaoAuthenticationProvider를 사용합니다.
- DaoAuthenticationProvider:
- DaoAuthenticationProvider의 authenticate 메서드가 호출되면, 먼저 retrieveUser 메서드를 통해 UserDetailsService를 호출하여 사용자 정보를 조회합니다.
- UserDetailsService:
- UserDetailsService의 loadUserByUsername 메서드가 호출되어 데이터베이스에서 사용자 정보를 조회하고, UserDetails 객체를 반환합니다.
- 추가 인증 검사 (Additional Authentication Checks):
- DaoAuthenticationProvider의 additionalAuthenticationChecks 메서드에서 입력된 비밀번호와 데이터베이스에서 조회된 비밀번호를 비교합니다.
- 이 과정에서 PasswordEncoder를 사용하여 입력된 비밀번호를 해싱하고, 저장된 해시된 비밀번호와 비교합니다.
- 인증 성공:
- 비밀번호가 일치하면 인증이 성공하며, UsernamePasswordAuthenticationToken 객체를 생성하여 반환합니다.
- 이 객체는 SecurityContextHolder에 저장되어 인증된 사용자 정보를 유지합니다.
package com.example.loans_domain.config;
import com.example.loans_domain.model.Customer;
import com.example.loans_domain.repository.CustomRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class UserDetailService implements UserDetailsService {
private final CustomRepository customRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String userName, password = null;
List<GrantedAuthority> authorities = null;
List<Customer> customer = customRepository.findByEmail(username);
if (customer.size() == 0) {
throw new UsernameNotFoundException("User details not found for the user : " + username);
} else {
userName = customer.get(0).getEmail();
password = customer.get(0).getPwd();
authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(customer.get(0).getRole()));
}
return new User(username, password, authorities);
}
}