Spring Security

AuthenticationProvider를 내 맘대로 커스터마이징할 수 있을까?

iksadnorth 2023. 9. 1. 20:20

👣 개요

결국 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에 있다.

 

GitHub - iksadNorth/lab-custom-provider-manager

Contribute to iksadNorth/lab-custom-provider-manager development by creating an account on GitHub.

github.com

 

👣 결론

기존에 있던 UsernamePasswordAuthenticationToken을 상속한 새로운 Authentication 객체를 사용하면 AuthenticationProvider를 커스터마이징할 수 있다. 하지만 굳이? 라는 생각이 든다.
차라리 Filter를 새로 하나 확장해서 커스터마이징 하는 것이 맘편할 것이다.