이 글은 정수원님의 Infrean 강의를 학습한 내용을 정리하여 작성합니다.

 

 

Ajax 인증 - AjaxAuthenticationProvider


  • AuthenticationProvider 인터페이스 구현
  • 인증 동작 조건
    • supports(Class<?> authentication)
      • ProvderManager로부터 넘어온 인증객체가 AjaxAuthenticationToken 타입이면 동작한다.
  • 인증 검증이 완료되면 AjaxAuthenticationToken을 생성해 최종 인증 객체를 반환한다.

 

 

구현


Ajax용 AjaxSecurityConfig 생성

AjaxSecurityConfig
@Configuration
@Order(0)
public class AjaxSecurityConfig {
    
    @Bean
    public SecurityFilterChain ajaxFilterChain(HttpSecurity http) throws Exception {
        http
                .antMatcher("/api/**")
                .authorizeHttpRequests((authz) -> authz
                        .anyRequest().authenticated()
                );

        http
                .addFilterBefore(new AjaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

 

참고
AjaxSecurityConfig 클래스는 모든 URL 정보에 대해 동작하는 것이 아니라 특정 URL ("/api/**")에 대해서만 동작하도록 antMatcher() 함수를 다음과 같이 호출한다.

 

주의!
설정 클래스를 여러개 생성하는 경우 동작 순서를 지정해줘야 한다.
@Order 애노테이션 사용

 

 

AjaxAuthenticationProvider 생성

AjaxAuthenticationProvider
  • Ajax 인증 방식 전용 AuthenticationProvider라 할지라도 Form 인증 방식과 별반 다를 게 없다.
  • 이전에 구현했떤 Form 인증 방식용 FormAuthenticationProvider 코드를 사용한다.
  • 다른점이 있다면 AuthenticationProvider에서 사용할 인증 객체가 UsernamePasswordAuthenticationToken이 아닌 우리가 생성한 Ajax 인증 방식용 인증 객체(AjaxAuthenticationToken)이라는 것이다.
public class AjaxAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        String loginId = authentication.getName();
        String password = (String)authentication.getCredentials();

        AccountContext accountContext = (AccountContext) userDetailsService.loadUserByUsername(loginId);

        // AuthenticationProvider는 사용자가 입력한 패스워드와 UserDetails의 패스워드와 비교한다.
        if (!passwordEncoder.matches(password, accountContext.getPassword())) {
            throw new BadCredentialsException("BadCredentialsException");
        }

        return new AjaxAuthenticationToken(accountContext.getAccount(), null, accountContext.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return AjaxAuthenticationToken.class.isAssignableFrom(authentication);
    }
}
  • supports
    • 전달받은 인증 객체가 AjaxAuthenticationToken 타입과 같을 때 동작한다.

 

설정 추가

  • 위에서 생성한 AjaxAuthenticationProvider 객체를 설정 파일에 등록한다.
AjaxSecurityConfig
@Configuration
@Order(0)
public class AjaxSecurityConfig {

    private AuthenticationConfiguration authenticationConfiguration;

    @Bean
    public AjaxAuthenticationProvider ajaxAuthenticationProvider() {
        return new AjaxAuthenticationProvider();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        ProviderManager authenticationManager = (ProviderManager)authenticationConfiguration.getAuthenticationManager();
        authenticationManager.getProviders().add(ajaxAuthenticationProvider());
        return authenticationManager;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .antMatcher("/api/**")
                .authorizeHttpRequests((authz) -> authz
                        .anyRequest().authenticated()
                );

        http
                .addFilterBefore(ajaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class);

        http.csrf().disable();

        return http.build();
    }

    @Bean
    public AjaxLoginProcessingFilter ajaxLoginProcessingFilter() throws Exception {
        AjaxLoginProcessingFilter ajaxLoginProcessingFilter = new AjaxLoginProcessingFilter();
        ajaxLoginProcessingFilter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
        return ajaxLoginProcessingFilter;
    }
}

 

 

실행


포스트맨 요청

 

  • 스프링 시큐리티는 FilterChainProxy 클래스를 통해 여러 보안 필터들을 관리한다.
    • 사용자 요청에 따라 그에 맞는 필터를 사용한다.
  • 디버깅 모드를 통해 FilterChainProxy가 가지고 있는 필터를 확인해보면 우리가 등록한 AjaxLoginProcessingFilter가 있는 것을 확인할 수 있다. 

 

 

AjaxLoginProcessingFilter 동작

  • AjaxLoginProcessingFilter가 동작해 사용자의 username과 password 정보가 담긴 Ajax 인증 객체가 생성된다.
  • 생성된 Ajax 인증 객체는 AuthenticationManager에게 전달된다.

 

 

AuthenticationManager 동작

  • AuthenticationManager 인터페이스 구현체인 ProviderManager 클래스가 동작하게 되고 autheticate() 함수가 호출된다.
  • 이때, 우리가 등록한 AjaxAuthentiationProvider가 존재하는 것을 확인할 수 있다.

 

AjaxAuthenticationProvider 동작

  • AjaxAuthenticationProvider가 동작하고 사용자의 username과 password를 정상적으로 받아온다.
  • 그리고 UserDetailsService로부터 AccountContext 정보 또한 정상적으로 가져온다.
  • AjaxAuthenticationProvider는 모든 인증 과정을 통과하면 새로운 AjaxAuthenticationToken을 생성해 다시 AuthenticationManager에게 반환한다.
  • 최종적으로 인증 Filter에게 반환된다.

 

결과

  • AjaxAuthenticationProvider에서 인증 처리를 수행하고 최종적으로 인증에 성공한 AjaxAuthenticationToken을 인증 필터에게 전달해 나머지 작업을 수행하고 인증 처리는 마무리된다.
  • 최종적으로 인증에 성공하면 현재 successHandler에서는 루트(/) 페이지로 이동하도록 리다이렉트 되어 있다.
    • 현재 우리는 REST를 사용한 비동기 Ajax 인증 방식을 사용하고 있어 리다이렉트 할 수 없다.
    • 이러한 문제를 해결하기 위해 Ajax 전용 successHandler를 생성해야 한다.