[개발지식] Web Application의 GateKeeper #14 - 인증 및 인가처리 후의 Spring Security의 Event publishing 과정 분석

Hyo Kyun Lee·2025년 12월 10일

개발지식

목록 보기
117/131

1. 개요

Spring Security는 인증 및 인가성공/실패에 대한 후처리를 위해 각각 이벤트를 발행한다.

이를 위해 ApplicationEvnetPublisher를 사용하거나 Spring Security에서 제공하는 AuthenticationEventPublisher 및 AuthorizationEventPublisher를 사용하여 발행해야 하며, 이에 대한 구현체인 DefaultAuthenticationEventPublisher 혹은 SpringAuthorizationEventPublisher를 활용해야 한다.

인증 및 인가처리 후의 비동기 처리를 위해 이벤트를 활용하는 방법에 대해 분석해보고자 한다.

2. Authentication Events

Spring Security는 인증 성공 및 실패 시 AuthenticationSuccessEvent 및 AuthenticationFailureEvent를 발생시킨다.

이 이벤트를 수신받기 위해서는 ApplicationEventPublisher 혹은 Spring Security에서 제공하는 AuthenticationEventPublisher를 사용하여 발행해야 하고, 이에 대한 구현체로 DefaultAuthenticationEventPublisher를 제공한다.

이벤트 수신을 위해

ApplicationEventPublisher.publishEvent(ApplicationEvent);

와 같이 Application level의 이벤트를 발행하거나

AuthenticationEventPublisher.publishAuthenticationSuccess(Authentication);
AuthenticationEventPublisher.publishAuthenticationFailure(AuthenticationException, Authentication);

와 같이 Spring Security에서 제공하는 AuthenticationEventPublisher를 통해 인증객체 및 인증예외에 대한 이벤트를 발행해주면 된다.

이에 대한 이벤트를 수신할때는

@Component 
public class AuthenticationEvents{
	@EventListener
    public void onSuccess(AuthenticationSuccessEvent success){
    ...
    }
    
    @EventListener
    public void onFailure(AbstractAuthenticationFailureEvent failures){
    	...
    }
}

와 같이 흔히 사용하는 EventListener와 함께 해당 이벤트를 구독하고, 성공 및 실패에 따른 후처리 로직을 구성해주면 된다.

참고로 후처리에 사용하는 인증성공 및 실패에 따른 이벤트는 다양한 클래스가 존재하며, 상속구조를 갖추고 있기에 부모클래스에서 제공하는 이벤트까지 모두 처리할 수 있다.

참고로, 인증성공 및 실패 이벤트를 모두 포함하는 최상위 추상클래스인

AbstractAuthenticationEvent
AbstractAuthenticationFailureEvent

를 제공하며,

이 추상클래스를 상속하는 다양한 인증성공(AuthenticationSuccessEvent/InteractiveAuthenticationSuccessEvent) 이벤트 및 인증실패(AuthenticationFailureBadCredentialsEvent 등) 이벤트가 존재하며, 우리는 이러한 이벤트를 사용하여 특정 후처리를 진행할 수 있다.

또한 인증성공 및 실패 이벤트를 모두 포함하는 최상위 부모 클래스를 이벤트로 사용한다면, 인증성공 및 실패 모든 상황에 대해 이벤트를 구독하기에 모든 이벤트를 항상 수신하게 된다는 점에 유의하자.

2-1. 인증성공 상황에 대한 실무 적용 방안

Spring Security의 이벤트 발행 및 구독은 Spring Application에서 제공하는 이벤트 발행 및 구독 동작원리를 그대로 채택하였기에, 기본적은 @EventListener 설정만으로도 이벤트 발행 및 구독은 가능하긴 하다.

예를 들어,

@Component
public class AuthenticationEvents {
    @EventListener
    public void onSuccess(AuthenticationSuccessEvent success) {
        System.out.println("Authentication SuccessEvent is published : success = " + success.getAuthentication().getName());
    }
    
    @EventListener
    public void onSuccess(InteractiveAuthenticationSuccessEvent success) {
        System.out.println("InteractiveAuthenticationSuccessEvent SuccessEvent is published : success = " + success.getAuthentication().getName());
    }
}

위와 같이 단순하게 EventListener를 등록하여, 해당 이벤트를 구독만 해주어도

로그인 성공(인증 성공) 시 위와 같이, 해당 이벤트에 대한 수신 및 후처리를 진행할 수 있다.

마찬가지 원리로, 기본적으로 제공하는 Spring application 차원에서의 Event 동작원리를 그대로 채택하여 인증 실패 경우에 대한 이벤트를 수신 및 후처리할 수 있다.

2-2. ProviderManager를 통한 Customized Event Publishing - 동작원리

지금까지 살펴본 부분은 기본적으로 제공하는 이벤트 구독 및 발행에 대한 부분이다.

실무에서 필요한 부분은, 여기에 나아가 Customized한 이벤트를 처리할 수 있는 지에 대한 여부일 것이다.

이를 위해 실질적인 인증처리를 수행하는 ProviderManager에서 인증(authenticate) 동작 시 내부를 살펴보면 어떻게 실무적으로 활용해야 할지 감이 잡힐 것이다.

try {
    Authentication result = authenticateWithProviders(...);

    // 인증 성공 시
    eventPublisher.publishAuthenticationSuccess(result);
    return result;

} catch (AuthenticationException ex) {

    // 인증 실패 시
    eventPublisher.publishAuthenticationFailure(ex, authentication);
    throw ex;
}

위와 같이, 인증성공에 대한 근거인 authentication 객체를 만들고 이를 반환하기 전에 eventPublisher를 통해 성공 및 실패 이벤트를 발행해주는 것을 확인할 수 있다.

private final ApplicationContext applicationEventPublisher;

더불어 applicationEventPublisher를 컴포넌트에 주입해주고,

.formLogin(form -> form.successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        applicationEventPublisher.publishEvent(new CustomizedAuthenticationSuccessEvent(authentication));
                        response.sendRedirect("/");
                    }
                }))

위와 같이 CustomizedAuthenticationSuccessEvent를 SuccessHandler에서 발행하여,

@EventListener
    public void onSuccess(CustomizedAuthenticationSuccessEvent success) {
        System.out.println("CustomizedAuthenticationSuccessEvent SuccessEvent is published : success = " + success.getAuthentication().getName());
    }

사용자 지정 이벤트 구독 및 발행에 따른 후처리를 구현할 수 있다.

이를 통해, 기본적으로 호출되는 이벤트 뿐만 아니라 CustomizedSuccessEvent까지 발행되어 이에 대한 후처리를 진행할 수 있음을 알 수 있다.

이것을 활용하여, 명시적으로 ProviderManager를 등록하여 이에 주입할 CustomizedAuthenticationEventPublisher까지 등록 후 사용하면 Customizing한 인증이벤트 발행 및 구독이 가능해진다.

[Authentication Filter]
        ↓
[AuthenticationManager (ProviderManager)]
        ↓
[AuthenticationEventPublisher]  ← 여기!
        ↓
[ApplicationEventPublisher]
        ↓
[@EventListener]

보안흐름은 위와 같은 과정으로 흐르기에, 이를 숙지하고 Spring Security에서 자동적으로 제공하는 Event Listener를 제공하기보다는, 웬만해선 요청사항에 맞게 Event를 Customizing하여 구성하는 것을 권장한다.

2-3. ProviderManager를 통한 Customized Event Publishing - 실무적용방안

먼저 CustomizedAuthenticationProvider를 구성하여, 인증과정 처리 시(authenticate) 특정조건, 특정조건에 대한 후처리(EventPublisher)를 구현한다.

@RequiredArgsConstructor
public class CustomizedAuthenticationProvider implements AuthenticationProvider {

    private final ApplicationContext applicationEventPublisher;

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

        if(!authentication.getName().equals("user")) {

            applicationEventPublisher.publishEvent
                    (new AuthenticationFailureProviderNotFoundEvent(authentication, new BadCredentialsException("BadCredentialException")));

            throw new BadCredentialsException("BadCredentialsException is thorwed by CustomizedAuthenticationProvider");
        }

        UserDetails user = User.withUsername("user").password("{noop}1111").roles("USER").build();
        return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
    }

이를 위해서는 위 클래스 객체를 authenticationProvider로 등록해주어야 하는데, Config 측에서

@Bean
    AuthenticationProvider authenticationProvider() {
        return new CustomizedAuthenticationProvider(applicationEventPublisher);
    }

위와 같이 빈객체를 등록하여

http
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/user").hasAuthority("ROLE_USER")
                        .requestMatchers("/db").hasAuthority("ROLE_DB")
                        .requestMatchers("/admin").hasAuthority("ROLE_ADMIN")
                        .anyRequest().authenticated() //secured/jsr보다 더 우선순위
                )
                .formLogin(form -> form.successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        applicationEventPublisher.publishEvent(new CustomizedAuthenticationSuccessEvent(authentication));
                        response.sendRedirect("/");
                    }
                }))
                .authenticationProvider(authenticationProvider())
                .csrf(AbstractHttpConfigurer::disable)
        ;

authenticationProvider에 해당 빈객체를 주입하여, Spring Security의 ProviderManager가 아닌 CustomizedProviderManager를 거치도록 등록해준다.

이를 테스트하면, 위에서 지정한 Exception이 throw하는 것을 확인할 수 있다.

2-4. Customized Event Publisher

Spring Security에서는 아래와 같이 DefaultAuthenticationEventPublisher와 같은 특정 구현체를 직접 빈객체로 정의할 수 있다.

@Bean
    public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        DefaultAuthenticationEventPublisher authenticationEventPublisher = new DefaultAuthenticationEventPublisher(applicationEventPublisher);
        return authenticationEventPublisher;
    }

내부적으로는 이 publisher를 통해 Exception 발생 시 어떠한 event 흐름으로 처리해야할지, 그 mapping 정보를 등록해준다.

따라서 CustomizedAuthenticationEventPublisher를 통해 Exception과 event를 사용자정의에 맞게 별도로 매핑해줄 수 있다(매핑정보가 없다면, Spring Security의 기본 failure event를 동작한다).

@Bean
    public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {

        Map<Class<? extends AuthenticationException>, Class<? extends AbstractAuthenticationFailureEvent>> mapping =
                Collections.singletonMap(CustomizedException.class, CustomizedAuthenticationFailureEvent.class);

        DefaultAuthenticationEventPublisher authenticationEventPublisher = new DefaultAuthenticationEventPublisher(applicationEventPublisher);
        authenticationEventPublisher.setAdditionalExceptionMappings(mapping);
        authenticationEventPublisher.setDefaultAuthenticationFailureEvent(DefaultAuthenticationFailureEvent.class); //default

        return authenticationEventPublisher;
    }

예를 들어 위와 같이, CustomizedException과 이에 따른 event정보를 mapping해줄 수 있으며, 해당 매핑정보가 없을때는 defaultAuthenticationFailureEvent로 지정한 이벤트를 발행한다.

3. Authorization Events

위에서 기술한 인증처리와 마찬가지로, 인가처리도 역시 Spring Security에서 인가 성공 및 실패(AccessDeniedException)에 대한 이벤트 발행 및 구독 행위를 지정해줄 수 있다.

마찬가지 원리로,

ApplicationEventPublisher.publishEvent(ApplicationEvent), 
AuthorizationEventPublisher.publishAuthorizationEvent(Supplier<Authentication>, T, AuthroizationDecision)

인가처리에 대한 publisher를 application level에서 진행하거나, spring security에서 진행해주는 인가 publisher가 특정 customized authorization 후처리를 진행하도록 등록해줄 수 있다.

인가이벤트 구독 및 발행의 경우, 인증과는 다르게 Publihser를 반드시 빈으로 등록해주어야 한다.

@Bean
public AuthorizationEventPublihser authorizationEventPublisher(ApplicationEventPublihser applicationEventPublihser){
	return new SpringAuthorizationEventPublihser(applicationEventPublisher);
}

그러나 위의 방법의 경우 인가실패에 대한 이벤트만 발행한다.

@Component
public class CustomizedAuthroizaionEventPublisher implements AuthorizationEventPublihser{
	...
}

AuthorizationEventPublisher를 직접 상속한 클래스를 빈객체(컴포넌트)로 등록하고, 내부적으로 authorizationDecision의 분기에 따라 인가실패 및 인가성공에 대한 후처리를 진행해주어야만 인가성공/실패 두 경우 모두에 대한 이벤트 발행 및 구독이 가능해짐에 유의하자.

4. 결론

Spring Security는 Spring Application에서 제공하는 Event Publishing/Subscribing 동작을 응용하여, 인증 및 인가처리에 대한 후처리 동작 구현 클래스 및 api를 제공한다.

앞서 살펴보았던 수단계에 걸친 정확한 인증/인가 본질에 대한 이해, 분석이 선행되었기에 인증/인가에 대한 후처리가 왜 필요한지 정확히 이해하고 해당 기능을 어떻게 구현할 수 있는지에 대해서도 문제없이 살펴볼 수 있었다.

이벤트 발행 및 구독에 대한 내용은 사실 독립적인 모듈로 구성되어있는 MSA환경에 적용하기 적합하지만, 어떠한 실무상황에서도 유연하게 확장성있는 응용역량을 함양할 수 있었다는 것에 그 의의를 두도록 하자.

0개의 댓글