👣 개요
결국 UserDetailsService를 사용하고 싶지 않아도
UsernamePasswordAuthenticationToken을 사용한다면 어쩔 수 없이
UserDetailsService를 사용해야 한다는 것이다.
만약 JWT에 Username을 넣으면 안 된다거나 Id값을 Payload에 넣고 싶다면 [혹은 등등의 어떠한 이유로]
UserDetailsService.loadUserByUsername()가 아닌 다른 인터페이스를 사용해야 할지도 모른다.
해당 상황에 대해 어떻게 대처를 할 수 있을까?
👣 AuthenicationManager 작동 원리 파악
우선 AuthenicationManager의 구현체, 즉 ProviderManager가 AuthenticationProvider를
선택하는 과정에 대해 알아야 한다.
우선 ProviderManager 내부적으로 AuthenticationProvider의 List를 보유하고 있고
AuthenticationProvider를 찾기 위해 providers 내부를 순회하며 적절한 AuthenticationProvider를 선별하여
하나의 AuthenticationProvider만 채택해 적용한다.
이 때, AuthenticationProvider를 선택하는 기준은
AuthenticationProvider.supports() 메서드의 true Return 여부이다.
만약 supports() 메서드가 false를 반환하면 continue를 통해 루프를 벗어나고
true를 반환하면 몇몇 가지의 과정을 거치고 채택해서 사용한다.
여기서 주목해야 할 점은 2개 이상의 AuthenticationProvider.supports() 메서드가 true를 반환할 지라도
순회 중 처음으로 맞닥뜨린 AuthenticationProvider만 선택해서 사용한다는 점이다.
때문에 만약 UserDetailsService를 사용하는 DaoAuthenticationProvider를 선택하지 않게 하기 위해선
단순히 내가 만든 CustomAuthenticationProvider를 ProviderManager에 등록하는 것에서 끝나지 않고
DaoAuthenticationProvider를 내부에서 제거해야지 의도한 바를 얻어낼 수 있다는 것이다.
👣 현실적인 구현 방법
하지만 기존의 AuthenticationProvider를 제거하는 방법에 대해 찾을 수 없었기 때문에
우회하여 문제를 해결하고자 한다.
새로운 전략은 차라리 새로운 Authentication 객체를 정의하고 해당 타입을
supports() 메서드에서 true로 Return하는 AuthenticationProvider를 새롭게 정의하고
추가하는 전략이다.
@Component
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final CustomUserDetailsService customUserDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 인증 여부 확인
String username = (String) authentication.getPrincipal();
String password = (String) authentication.getCredentials();
Optional<UserDetails> optionalUserDetails = customUserDetailsService.loadUserBySomething(username, password);
UserDetails principal = optionalUserDetails.orElseThrow(() -> new IllegalArgumentException("유저가 없거나 비밀 번호가 틀렸을 수 있음."));
// 인증 객체로 리뉴얼
return CustomAuthenticationToken.authenticated(principal, principal.getPassword(), principal.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.isAssignableFrom(CustomAuthenticationToken.class);
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
AuthenticationConfiguration authenticationConfiguration;
@Autowired
AuthenticationManagerBuilder builder;
@Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
@PostConstruct
public void authenticationManager() {
builder.authenticationProvider(customAuthenticationProvider);
}
@Bean
public CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
return new CustomAuthenticationFilter("/api/v1/login", authenticationConfiguration.getAuthenticationManager());
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정
http.csrf(AbstractHttpConfigurer::disable);
http.authorizeHttpRequests((authorizeHttpRequests) ->
authorizeHttpRequests
.requestMatchers("/api/v1/login").permitAll()
.anyRequest().authenticated()
);
http.addFilterBefore(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
좀 더 좋은 방법이 있을 것 같기도 한데 아직 그런 것을 찾지 못해 이렇게 구현했다.
이에 대한 기록은 다음 git repo에 있다.
👣 결론
기존에 있던 UsernamePasswordAuthenticationToken을 상속한 새로운 Authentication 객체를 사용하면 AuthenticationProvider를 커스터마이징할 수 있다. 하지만 굳이? 라는 생각이 든다.
차라리 Filter를 새로 하나 확장해서 커스터마이징 하는 것이 맘편할 것이다.
'Spring Security' 카테고리의 다른 글
Spring Security Configuration 실험 (0) | 2023.09.30 |
---|---|
인증 필터는 어떻게 재정의할 수 있을까? (0) | 2023.09.01 |
UserDetailsService 구현은 항상 요구되는 것인가? (0) | 2023.09.01 |
인증 관련 Architecture 분석 (0) | 2023.09.01 |