Spring Security는 인증 및 인가성공/실패에 대한 후처리를 위해 각각 이벤트를 발행한다.
이를 위해 ApplicationEvnetPublisher를 사용하거나 Spring Security에서 제공하는 AuthenticationEventPublisher 및 AuthorizationEventPublisher를 사용하여 발행해야 하며, 이에 대한 구현체인 DefaultAuthenticationEventPublisher 혹은 SpringAuthorizationEventPublisher를 활용해야 한다.
인증 및 인가처리 후의 비동기 처리를 위해 이벤트를 활용하는 방법에 대해 분석해보고자 한다.
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 등) 이벤트가 존재하며, 우리는 이러한 이벤트를 사용하여 특정 후처리를 진행할 수 있다.
또한 인증성공 및 실패 이벤트를 모두 포함하는 최상위 부모 클래스를 이벤트로 사용한다면, 인증성공 및 실패 모든 상황에 대해 이벤트를 구독하기에 모든 이벤트를 항상 수신하게 된다는 점에 유의하자.
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 동작원리를 그대로 채택하여 인증 실패 경우에 대한 이벤트를 수신 및 후처리할 수 있다.
지금까지 살펴본 부분은 기본적으로 제공하는 이벤트 구독 및 발행에 대한 부분이다.
실무에서 필요한 부분은, 여기에 나아가 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하여 구성하는 것을 권장한다.
먼저 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하는 것을 확인할 수 있다.
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로 지정한 이벤트를 발행한다.
위에서 기술한 인증처리와 마찬가지로, 인가처리도 역시 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의 분기에 따라 인가실패 및 인가성공에 대한 후처리를 진행해주어야만 인가성공/실패 두 경우 모두에 대한 이벤트 발행 및 구독이 가능해짐에 유의하자.
Spring Security는 Spring Application에서 제공하는 Event Publishing/Subscribing 동작을 응용하여, 인증 및 인가처리에 대한 후처리 동작 구현 클래스 및 api를 제공한다.
앞서 살펴보았던 수단계에 걸친 정확한 인증/인가 본질에 대한 이해, 분석이 선행되었기에 인증/인가에 대한 후처리가 왜 필요한지 정확히 이해하고 해당 기능을 어떻게 구현할 수 있는지에 대해서도 문제없이 살펴볼 수 있었다.
이벤트 발행 및 구독에 대한 내용은 사실 독립적인 모듈로 구성되어있는 MSA환경에 적용하기 적합하지만, 어떠한 실무상황에서도 유연하게 확장성있는 응용역량을 함양할 수 있었다는 것에 그 의의를 두도록 하자.