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"); } }