AuthenticationEventPublisher (이벤트 생성 주체)
- 인증 성공 또는 실패가 발생했을 때 이벤트를 전달하기 위한 이벤트 퍼블리셔 인터페이스
- 기본 구현체로 DefaultAuthenticationEventPublisher 클래스가 사용됨
- ProviderManager에서 호출이 되게 됨
public interface AuthenticationEventPublisher { void publishAuthenticationSuccess(Authentication authentication); void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication); }
이벤트의 종류
AuthenticationSuccessEvent
— 로그인 성공 이벤트
AbstractAuthenticationFailureEvent
— 로그인 실패 이벤트 (실패 이유에 따라 다양한 구체 클래스가 정의되 있음)
이벤트에 대해서 반응하도록 하는 방법1. successfulAuthentication 메소드를 override (혹은 AuthenticationSuccessHandler 재정의)2. 이벤트 리스너 추가비동기로 이벤트 리스너 등록방법(@Async, @EnableAsync)
이벤트에 대해서 반응하도록 하는 방법
인증 성공 또는 실패가 발생했을 때 관련 이벤트(ApplicationEvent)가 발생하고, 해당 이벤트에 관심있는 컴포넌트는 이벤트를 구독할 수 있다.
주의해야 할 부분은 Spring의 이벤트 모델이 동기적이라는 것이다. 따라서 이벤트를 구독하는 리스너의 처리 지연은 이벤트를 발생시킨 요청의 응답 지연에 직접적인 영향을 미친다.
그렇다면 왜 이벤트 모델을 사용해야 할까? 이벤트 모델은 컴포넌트 간의 느슨한 결합을 유지하는데 도움을 준다. 예를들어 로그인 성공 시 사용자에게 이메일을 발송해야 하는 시스템을 생각해보자. 우리는 이제 Spring Security의 인프라스트럭처를 잘 이해하고 있으므로 최대한 이를 이용하려 할 것이다.
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(authResult); SecurityContextHolder.setContext(context); if (this.logger.isDebugEnabled()) { this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult)); } this.rememberMeServices.loginSuccess(request, response, authResult); if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } this.successHandler.onAuthenticationSuccess(request, response, authResult); }
1. successfulAuthentication 메소드를 override (혹은 AuthenticationSuccessHandler 재정의)
- AbstractAuthenticationProcessingFilter 추상 클래스를 상속하고, 인증이 성공했을 때 수행되는 successfulAuthentication 메소드를 override 함
- 또는 AuthenticationSuccessHandler를 재정의할 수 있음
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { sendEmail(authResult); super.successfulAuthentication(request, response, chain, authResult); }
그런데 어느날, 로그인 성공 시 이메일 뿐만 아니라 SMS 전송도 함께 이루어져야 한다는 요구사항을 받았다. 우리는 앞서 만들었던 successfulAuthentication 메소드를 수정하기로 한다.
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { sendEmail(authResult); sendSms(authResult); super.successfulAuthentication(request, response, chain, authResult); }
이 처럼 요구사항이 변화할 때 관련 코드를 지속해서 수정해야하는 것은 해당 코드가 높은 결합도를 가지고 있고, 확장에 닫혀 있기 때문이다. 이 문제를 이벤트 발생-구독 모델로 접근한다면 Spring Security의 인프라스트럭처 위에서 수정해야 하는 것은 아무것도 없다. 단지 인증 성공 이벤트를 구독하는 리스너를 추가만 하면된다.
2. 이벤트 리스너 추가
- 이메일 발송 리스너 — 로그인 성공 이벤트를 수신하고, 이메일을 발송함
- SMS 발송 리스너 — 로그인 성공 이벤트를 수신하고, SMS를 발송함
또 다른 발송 채널을 추가해야 한다면 기존 코드는 수정할 필요가 없다. 그저 필요한 리스너를 추가하면된다.
이벤트 리스너 등록 방법
- @EventListener 어노테이션을 이용하여 리스너 등록
@Component public class CustomAuthenticationEventHandler { private final Logger log = LoggerFactory.getLogger(getClass()); @EventListener public void handleAuthenticationSuccessEvent(AuthenticationSuccessEvent event) { Authentication authentication = event.getAuthentication(); log.info("Successful authentication result: {}", authentication.getPrincipal()); } @EventListener public void handleAuthenticationFailureEvent(AbstractAuthenticationFailureEvent event) { Exception e = event.getException(); Authentication authentication = event.getAuthentication(); log.warn("Unsuccessful authentication result: {}", authentication, e); } }
- 주의해야 할 점은 Spring의 이벤트 모델이 동기적이기 때문에 이벤트를 구독하는 리스너에서 처리가 지연되면, 이벤트를 발행하는 부분 처리도 지연됨
- @EnableAsync로 비동기 처리를 활성화하고, @Async 어노테이션을 사용해 이벤트 리스너를 비동기로 변경할 수 있음
비동기로 이벤트 리스너 등록방법(@Async, @EnableAsync)
@Async @EventListener public void handleAuthenticationSuccessEvent(AuthenticationSuccessEvent event){ Authentication authentication = event.getAuthentication(); log.info("Successful Authentication : {}", authentication); } @Configuration @EnableAsync public class WebMvcConfigure implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("index"); registry.addViewController("/me").setViewName("me"); registry.addViewController("/admin").setViewName("admin"); } }