개요
저번 글에 이어서 이번에는 로그인 인증에 실패하면 작동하는 AuthenticationFailureHandler에 대해서 알아보려고 한다.
AuthenticationFailureHandler란?
public interface AuthenticationFailureHandler {
void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException;
}
위의 코드에서 보이는 것과 같이 AuthenticationFailureHandler 인터페이스는 onAuthenticationFailure 메서드를 제공한다.
AuthenticationSuccessHandler처럼 해당 기능을 등록하기 위해서는 SecurityFilterChain에 해당 인터페이스의 구현체를 등록해야 한다.
@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class SecurityConfig {
// 생략
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.userInfoEndpoint(userInfo -> userInfo.userService(oauth2UserService))
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler) // AuthenticationFailureHandler 적용
)
// 생략
}
}
이렇게 하면 로그인 성공 후 인증 완료 시 위에서 말한 AuthenticationFailureHandler의 onAuthenticationFailure 메서드가 실행되어 해당 메서드에 구현한 로직이 실행된다.
AuthenticationFailureHandler 구현체
Spring은 여러 AuthenticationFailureHandler 인터페이스의 구현체를 제공한다.
ForwardAuthenticationFailureHandler
public class ForwardAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final String forwardUrl;
public ForwardAuthenticationFailureHandler(String forwardUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl), () -> "'" + forwardUrl + "' is not a valid forward URL");
this.forwardUrl = forwardUrl;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
request.getRequestDispatcher(this.forwardUrl).forward(request, response);
}
}
이 구현체는 단순하게 인증에 실패할 경우 생성자에서 지정해 놓은 url로 forward하는 기능을 제공한다. ForwardAuthenticationSuccessHandler와 다른 점은 forward 실행 전, 인증 실패 시 발생한 AuthenticationException을 request.setAtttribute()로 설정한다는 것이다.
SimpleUrlAuthenticationFailureHandler
public class SimpleUrlAuthenticationFailureHandler implements AuthenticationFailureHandler {
protected final Log logger = LogFactory.getLog(getClass());
private String defaultFailureUrl;
private boolean forwardToDestination = false;
private boolean allowSessionCreation = true;
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
public SimpleUrlAuthenticationFailureHandler() {
}
public SimpleUrlAuthenticationFailureHandler(String defaultFailureUrl) {
setDefaultFailureUrl(defaultFailureUrl);
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
if (this.defaultFailureUrl == null) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Sending 401 Unauthorized error since no failure URL is set");
}
else {
this.logger.debug("Sending 401 Unauthorized error");
}
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
return;
}
saveException(request, exception);
if (this.forwardToDestination) {
this.logger.debug("Forwarding to " + this.defaultFailureUrl);
request.getRequestDispatcher(this.defaultFailureUrl).forward(request, response);
}
else {
this.redirectStrategy.sendRedirect(request, response, this.defaultFailureUrl);
}
}
protected final void saveException(HttpServletRequest request, AuthenticationException exception) {
if (this.forwardToDestination) {
request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
return;
}
HttpSession session = request.getSession(false);
if (session != null || this.allowSessionCreation) {
request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
}
}
public void setDefaultFailureUrl(String defaultFailureUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(defaultFailureUrl),
() -> "'" + defaultFailureUrl + "' is not a valid redirect URL");
this.defaultFailureUrl = defaultFailureUrl;
}
protected boolean isUseForward() {
return this.forwardToDestination;
}
public void setUseForward(boolean forwardToDestination) {
this.forwardToDestination = forwardToDestination;
}
public void setRedirectStrategy(RedirectStrategy redirectStrategy) {
this.redirectStrategy = redirectStrategy;
}
protected RedirectStrategy getRedirectStrategy() {
return this.redirectStrategy;
}
protected boolean isAllowSessionCreation() {
return this.allowSessionCreation;
}
public void setAllowSessionCreation(boolean allowSessionCreation) {
this.allowSessionCreation = allowSessionCreation;
}
}
SimpleUrlAuthenticationFailureHandler는 SimpleUrlAuthenticationSuccessHandler와는 달리 더 다양한 기능들을 제공한다. 해당 기능들을 바탕으로 인증에 실패할 경우에 onAuthenticationFailure 메서드 안에서 다양한 로직이 수행된다.
처음에는 defaultFailureUrl이 지정되어 있는지 확인한다. 만약 지정된 defaultFailureUrl이 없다면, 401 Unauthorized Error를 응답하고 끝낸다. defaultFailureUrl이 존재한다면, 인증 실패 시 발생하는 AuthenticationException을 session에 저장하는 기능을 수행하는 saveException 메서드를 호출한다.
- saveException 메서드 안에서는 forwardToDestination이 false이고 allowSessionCreation이 true일 경우에만 session에 예외를 저장하게 된다.
이후에는 다시 onAuthenticationFailure로 돌아와 forwardToDestination이 true로 설정되어 있을 경우 defaultFailureUrl로 forward를 진행하며, false로 설정될 경우에는 redirect를 진행한다.
DelegatingAuthenticationFailureHandler
public class DelegatingAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final LinkedHashMap<Class<? extends AuthenticationException>, AuthenticationFailureHandler> handlers;
private final AuthenticationFailureHandler defaultHandler;
public DelegatingAuthenticationFailureHandler(
LinkedHashMap<Class<? extends AuthenticationException>, AuthenticationFailureHandler> handlers,
AuthenticationFailureHandler defaultHandler) {
Assert.notEmpty(handlers, "handlers cannot be null or empty");
Assert.notNull(defaultHandler, "defaultHandler cannot be null");
this.handlers = handlers;
this.defaultHandler = defaultHandler;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
for (Map.Entry<Class<? extends AuthenticationException>, AuthenticationFailureHandler> entry : this.handlers
.entrySet()) {
Class<? extends AuthenticationException> handlerMappedExceptionClass = entry.getKey();
if (handlerMappedExceptionClass.isAssignableFrom(exception.getClass())) {
AuthenticationFailureHandler handler = entry.getValue();
handler.onAuthenticationFailure(request, response, exception);
return;
}
}
this.defaultHandler.onAuthenticationFailure(request, response, exception);
}
}
이 구현체는 인증 실패 시 onAuthenticationFailure 메서드로 전달되는 AuthenticationException의 타입에 따라 다른 AuthenticationFailureHandler로 위임하는 기능을 갖고 있다. 만약 AuthenticationException에 따라 설정해둔 AuthenticationFailureHandler가 없다면, 생성자에서 설정한 defaultHandler에게로 위임한다.
ExceptionMappingAuthenticationFailureHandler
public class ExceptionMappingAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private final Map<String, String> failureUrlMap = new HashMap<>();
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String url = this.failureUrlMap.get(exception.getClass().getName());
if (url != null) {
getRedirectStrategy().sendRedirect(request, response, url);
}
else {
super.onAuthenticationFailure(request, response, exception);
}
}
public void setExceptionMappings(Map<?, ?> failureUrlMap) {
this.failureUrlMap.clear();
for (Map.Entry<?, ?> entry : failureUrlMap.entrySet()) {
Object exception = entry.getKey();
Object url = entry.getValue();
Assert.isInstanceOf(String.class, exception, "Exception key must be a String (the exception classname).");
Assert.isInstanceOf(String.class, url, "URL must be a String");
Assert.isTrue(UrlUtils.isValidRedirectUrl((String) url), () -> "Not a valid redirect URL: " + url);
this.failureUrlMap.put((String) exception, (String) url);
}
}
}
이 클래스는 setExceptionMappings 메서드를 통해 Exception의 이름과 redirect할 URL을 failureUrlMap으로 저장하는 기능을 제공한다. onAuthenticationFailure에서는 failureUrlMap을 바탕으로 전달된 AuthenticationException의 이름과 매칭되는 redirect URL을 찾는다. URL이 존재할 경우 해당 URL로 redirect하고, 찾지 못했을 경우에는 상위 클래스에게 역할을 위임한다.
AuthenticationEntryPointFailureHandler
public class AuthenticationEntryPointFailureHandler implements AuthenticationFailureHandler {
private boolean rethrowAuthenticationServiceException = true;
private final AuthenticationEntryPoint authenticationEntryPoint;
public AuthenticationEntryPointFailureHandler(AuthenticationEntryPoint authenticationEntryPoint) {
Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null");
this.authenticationEntryPoint = authenticationEntryPoint;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
if (!this.rethrowAuthenticationServiceException) {
this.authenticationEntryPoint.commence(request, response, exception);
return;
}
if (!AuthenticationServiceException.class.isAssignableFrom(exception.getClass())) {
this.authenticationEntryPoint.commence(request, response, exception);
return;
}
throw exception;
}
public void setRethrowAuthenticationServiceException(boolean rethrowAuthenticationServiceException) {
this.rethrowAuthenticationServiceException = rethrowAuthenticationServiceException;
}
}
마무리
AuthenticationFailureHandler 인터페이스도 다양한 구현체를 제공하기 때문에 상황에 맞게 구현체를 골라서 사용하거나 상속받아서 직접 핸들러를 만들면 인증 실패 로직을 구현할 수 있을 것이다.
'Spring > Spring Security' 카테고리의 다른 글
[Spring Security] Method Security - 메서드 레벨에서 권한을 확인하는 방법 (0) | 2024.06.03 |
---|---|
[Spring Security] AuthenticationSuccessHandler에 대하여 (0) | 2024.03.15 |