AuthenticationManager 방식의 JWT 인증
시큐리티 인증 클래스들의 상호작용

시큐리티 인증 클래스들의 상호작용

AuthenticationManager, AuthenticationProvider, ProviderManager

ProviderManager 는 AuthenticationProvider 인스턴스들을 List 형태로 가지고있으면서 관리하는 객체입니다. 즉 Provider 들을 Manage 하는 객체입니다. 이 ProviderManagerAuthenticationManager 인터페이스를 구현한 기본 구현체입니다.

AuthenticationManagerResolver

AuthenticationManager 를 기본 구현한 클래스는 ProviderManager 이고 스프링에서 기본으로 제공하지만, 여러가지 종류의 AuthenticationManager 를 구현해서 사용하고 싶을 수 있습니다. 인증 로직이 복잡해질 수록 한군데에 하드코딩한 로직들은 점점 갈 수록 장애를 내게 될 확률이 높기에 시기 적절하게 항상 기능별로 모듈화를 해두는 것이 좋습니다. 따라서 용도별로 AuthenticationManager 를 분리해서 정의하는 경우가 많습니다.

이렇게 용도별로 분리해둔 AuthenticationManager 들 중에서 Filter 가 어떤 AuthenticationManager 를 사용할지 결정할 수 있도록 하는 역할을 하는 것이 AuthenticationManagerResolver 입니다. 말 그대로 AuthenticationManager 를 resolve 하는 역할을 수행합니다.


예를 들어 아래와 같은 시큐리티 클래스가 있다고 해보겠습니다.

@Configuration
public class SucurityConfig {
    // ...
}

그리고 customerAuthenticationManager 라는 이름의 Bean 을 아래와 같이 생성한다고 해보겠습니다. 예제이기에 간단하게 인라인 형식으로 단순하게 만들었습니다.

@Bean
AuthenticationManager customersAuthenticationManager() {
    return authentication -> {
        if (isCustomer(authentication)) {
            return new UsernamePasswordAuthenticationToken(/*credentials*/);
        }
        throw new UsernameNotFoundException(/*principal name*/);
    };
}

이번에는 employeesAuthenticationManager 라는 이름의 Bean 을 아래와 같이 생성해봅니다. 예제 수준의 단순한 인라인 정 코드입니다.

@Bean
public AuthenticationManager employeesAuthenticationManager() {
    return authentication -> {
        if (isEmployee(authentication)) {
            return new UsernamePasswordAuthenticationToken(/*credentials*/);
        }
        throw new UsernameNotFoundException(/*principal name*/);
    };
}

이렇게 정의해둔 AuthenticationManager 들은 어떤 기준에 의해 선택이 될수 있어야 합니다. AuthenticationManagerResolver 에는 이런 AuthenticationManager 들을 특정 기준에 의해 선택하는 코드를 작성합니다.

AuthenticationManagerResolver<HttpServletRequest> resolver() {
    return request -> {
        if (request.getPathInfo().startsWith("/employee")) {
            return employeesAuthenticationManager();
        }
        return customersAuthenticationManager();
    };
}

ReactiveAuthenticationManagerResolver

이번에는 위에서 살펴본 AuthenticationManager 를 Reactive 버전으로 작성해봅니다.

예를 들어 아래와 같은 시큐리티 클래스가 있다고 해보겠습니다. 아까와 달라진 점은 @EnableWebFluxSecurity, @EnableReactiveMethodSecurity 을 추가해줬다는 점입니다.

@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SucurityConfig {
    // ...
}

그리고 customerAuthenticationManager 라는 이름의 Bean 을 아래와 같이 생성한다고 해보겠습니다. 예제이기에 간단하게 인라인 형식으로 단순하게 만들었습니다. 아까와 달라진 점은 ReactiveAuthenticationManager 를 반환한다는 점입니다.

@Bean
public ReactiveAuthenticationManager customersAuthenticationManager() {
    return authentication -> {
        if (isCustomer(authentication)) {
            return new UsernamePasswordAuthenticationToken(/*credentials*/);
        }
        throw new UsernameNotFoundException(/*principal name*/);
    };
}

이번에는 employeesAuthenticationManager 라는 이름의 Bean 을 아래와 같이 생성해봅니다. 예제 수준의 단순한 인라인 코드입니다. 아까와 달라진 점은 ReactiveAuthenticationManager 를 반환한다는 점입니다.

@Bean
public ReactiveAuthenticationManager employeesAuthenticationManager() {
    return authentication -> {
        if (isEmployee(authentication)) {
            return new UsernamePasswordAuthenticationToken(/*credentials*/);
        }
        throw new UsernameNotFoundException(/*principal name*/);
    };
}

이렇게 정의한 ReactiveAuthenticationManager 들은 아래와 같이 ReactiveAuthenticationManagerResolver 에서 적절한 ReactiveAuthenticationManager 를 선택하도록 정의하는 것이 가능합니다.

ReactiveAuthenticationManagerResolver<ServerWebExchange> resolver() {
    return exchange -> {
        if (match(exchange.getRequest(), "/employee")) {
            return Mono.just(employeesAuthenticationManager());
        }
        return Mono.just(customersAuthenticationManager());
    };
}

foobar-user 는?

제가 작성한 foobar-user 에서는 ReactiveAuthenticationManagerResolver 까지 사용하지는 않았고 1개의 ReactiveAuthenticationManager를 Filter 에 등록해서 사용하는 방식으로 아래와 같이 사용했습니다.

@RequiredArgsConstructor
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
@Configuration
public class SecurityConfig {
 
  @Bean
  public SecurityWebFilterChain filterChain(
      ServerHttpSecurity httpSecurity,
      JwtServerAuthenticationConverter converter,
      JwtAuthenticationManager authenticationManager
  ){
    // (1)
    var authenticationWebFilter = new AuthenticationWebFilter(authenticationManager);
    authenticationWebFilter.setServerAuthenticationConverter(converter);
 
    return httpSecurity
        // ...
        .csrf(csrfSpec -> csrfSpec.disable())
        .formLogin(formLoginSpec -> formLoginSpec.disable())
        .httpBasic(httpBasicSpec -> httpBasicSpec.disable())
        // ...
        .addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
        .build();
  }
}

(1)

  • AuthenticationWebFilter 객체 생성시에 JwtAuthenticationManager 를 전달해줍니다.
  • JwtAuthenticationManager 의 내용은 아래에 정리해두었습니다.

아래는 JwtAuthenticationManager 클래스의 내용입니다. 이 코드에 대한 자세한 설명은 ReactiveAuthenticationManager방식의 JWT 인증 # JwtAuthenticationManager (opens in a new tab) 문서에 정리해두었습니다.

package io.chagchagchag.example.foobar.user.config.security;
 
// ...
 
@RequiredArgsConstructor
@Component
public class JwtAuthenticationManager implements ReactiveAuthenticationManager {
  private final JwtSupport jwtSupport;
  private final CustomUserDetailsService userDetailsService;
 
  @Override
  public Mono<Authentication> authenticate(Authentication authentication) {
    return Mono.justOrEmpty(authentication)
        .filter(auth -> auth instanceof BearerToken)
        .cast(BearerToken.class)
        .map(bearerToken -> degenerateToken(bearerToken))
        .flatMap(jwtDto -> validateJwt(jwtDto))
        .flatMap(jwtDto -> findUserById(jwtDto.id()))
        .onErrorMap(throwable -> new IllegalArgumentException("INVALID JWT"));
  }
 
  public JwtDto degenerateToken(BearerToken token){
    return jwtSupport.degenerateToken(SecurityProperties.key, token.getJwt());
  }
 
  public Mono<JwtDto> validateJwt(JwtDto jwtDto){
    if(jwtSupport.checkIfNotExpired(jwtDto.expiration())){
      return Mono.just(jwtDto);
    }
    return Mono.error(new IllegalArgumentException("Token Invalid"));
  }
 
  private Mono<Authentication> findUserById(String userId){
    return userDetailsService
        .findByUsername(userId)
        .map(userDetails -> {
          var authentication = new UsernamePasswordAuthenticationToken(
              userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities()
          );
          SecurityContextHolder.getContext().setAuthentication(authentication);
          return authentication;
        });
  }
  
}