본문 바로가기

Programming/Spring Security

[Spring Security][회원가입 및 로그인 예제 6/9] AuthenticationSuccessHandler & AuthenticationFailureHandler 구현

들어가며...

로그인 성공 또는 실패 시 상황에 맞게 좀 더 세부적으로 수행해야 할 작업들이 있을 수 있습니다.

예를 들면 로그인 실패 시 실패 원인을 UI에 전달하여 화면에 보여주고 싶을 수도 있고 로그인이 일정한 회수 이상 실패할 경우 계정잠금을 해야 할 경우도 있을 수 있습니다.

반대로 로그인 성공 시에는 앞서 저장한 로그인 실패 회수를 초기화하거나 인증권한에 따라 서로 다른 페이지를 보여주고 싶을 수도 있습니다.

 

앞서 포스팅한 WebSecurityConfigurerAdapter에서도 로그인 성공 시 또는 실패 시 보여줄 파일 패스를 설정할 수도 있으나 위에서 언급한 좀 더 세부적인 작업을 하고자 할 경우에는 Spring Security에서 마련해논 AuthenticationSuccessHandler 및 AuthenticationFailureHandler 인터페이스를 implements하여 필요한 작업을 구현하면 됩니다.

 

본 글에서는 해당 인터페이스를 DI하고 간단한 기능을 구현하는 방법에 대해서 살펴보도록 하겠습니다.

 

  • Table of Contents
    • Configuration
    • AuthenticationSuccessHandler 구현
    • AuthenticationFailureHandler 구현

Configuration

● SecurityConfig.java

package com.demo.security.auth.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.demo.security.auth.eo.ERole;
import com.demo.security.auth.handler.CustomAuthFailureHandler;
import com.demo.security.auth.handler.CustomAuthSuccessHandler;
import com.demo.security.auth.service.CustomUserDetailsService;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Autowired
	private BCryptPasswordEncoder bCryptPasswordEncoder;
	@Autowired
	private CustomUserDetailsService userDetailsService;
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userDetailsService)
			.passwordEncoder(bCryptPasswordEncoder);
	}
	
	@Override
	public void configure(WebSecurity web) throws Exception {
		web.ignoring()
			.antMatchers("/css/**", "/js/**", "/images/**", "/lib/**");
	}
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests()
				.antMatchers("/", "/login", "/registration", "/h2/**").permitAll()
				.antMatchers("/home/admin").hasAuthority(ERole.ADMIN.getValue())
				.antMatchers("/home/user").hasAuthority(ERole.MANAGER.getValue())
				.antMatchers("/home/guest").hasAuthority(ERole.GUEST.getValue())
				.anyRequest().authenticated()
				.and()
			.csrf()
				.disable()
			.headers()
				.frameOptions().disable()
				.and()
			.formLogin()
				.loginPage("/login")
				.defaultSuccessUrl("/home")
				.failureUrl("/login?error=true")
                //< 아래에서 등록한 AuthenticationSuccessHandler 설정
				.successHandler(successHandler())
                //< 아래에서 등록한 AuthenticationFailureHandler 설정
				.failureHandler(failureHandler())
				.usernameParameter("username")
				.passwordParameter("password")
				.and()
			.logout()
				.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
				.logoutSuccessUrl("/login")
				.and()
			.exceptionHandling()
				.accessDeniedPage("/access-denied");
	}
	
	@Bean
	public BCryptPasswordEncoder passwordEncoder() {
		BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
		return bCryptPasswordEncoder;
	}
	
    ///////////////////////////////////////////////////////////////////
    //< 성공, 실패 Handler를 @Bean으로 등록 (DI)
    
	@Bean
	public AuthenticationSuccessHandler successHandler() {
		return new CustomAuthSuccessHandler();
	}
	
	@Bean
	public AuthenticationFailureHandler failureHandler() {
		return new CustomAuthFailureHandler();
	}
}

 

 

위 코드의 주석처리된 내용과 같이 인증 성공 및 실패 핸들러를 구현하려면 해당 인터페이스를 @Bean으로 등록한 후 해당 함수를 Configuration의 "formLogin()"에 "successHandler(), failureHandler()"에 설정하여야 한다.

AuthenticationSuccessHandler 구현

● CustomAuthSuccessHandler.java

package com.demo.security.auth.handler;

import java.io.IOException;
import java.util.Set;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;

import com.demo.security.auth.eo.ERole;

public class CustomAuthSuccessHandler implements AuthenticationSuccessHandler {
	private RequestCache requestCache = new HttpSessionRequestCache();
	private RedirectStrategy redirectStratgy = new DefaultRedirectStrategy();
	private final String DEFAULT_LOGIN_SUCCESS_URL = "/home";
	
	////////////////////////////////////////////////////////////////////////////////
	//< public functions (override)

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
		//< clear authentication error
		clearAuthenticationAttributes(request);
		//< redirect page
		redirectStrategy(request, response, authentication);
	}
	
	////////////////////////////////////////////////////////////////////////////////
	//< private functions
	
	private void clearAuthenticationAttributes(HttpServletRequest request) {
		HttpSession session = request.getSession(false);
		if(session != null) {
			session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
		}
	}
	
	private void redirectStrategy(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
		//< get the saved request
		SavedRequest savedRequest = requestCache.getRequest(request, response);
		if(savedRequest == null) {
			redirectStratgy.sendRedirect(request, response, DEFAULT_LOGIN_SUCCESS_URL);
		}
		else {
			//< get the authorities
			Set<String> roles = AuthorityUtils.authorityListToSet(authentication.getAuthorities());
			if(roles.contains(ERole.ADMIN.getValue())) {
				redirectStratgy.sendRedirect(request, response, "/home/admin");
			}
			else if(roles.contains(ERole.MANAGER.getValue())) {
				redirectStratgy.sendRedirect(request, response, "/home/user");
			}
			else {
				redirectStratgy.sendRedirect(request, response, "/home/guest");
			}
		}
	}
}

 

위의 코드를 간략하게 설명하면 다음과 같습니다.

  • 이전에 로그인 실패한 이력이 있을 경우 session에 남아있는 실패 이력을 제거
  • 바로 로그인 페이지로 접근한 경우 : 로그인 성공 시 "/home" 파일 패스로 페이지 이동
  • 인증이 필요한 페이지 접근 시 미인증 상태여서 로그인 페이지로 이동된 경우 : 로그인한 사용자의 접근권한에 따른 페이지로 이동

AuthenticationFailuerHandler 구현

● CustomAuthFailureHandler.java

package com.demo.security.auth.handler;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

public class CustomAuthFailureHandler implements AuthenticationFailureHandler {
	private final String DEFAULT_FAILURE_URL = "/login?error=true";

	////////////////////////////////////////////////////////////////////////////////
	//< public functions (constructor)
	
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
		String errorMessage = null;
		
		//=================================================
		//< set the error message
		//=================================================
		//< incorrect the identify or password
		if(exception instanceof BadCredentialsException || exception instanceof InternalAuthenticationServiceException) {
			errorMessage = "아이디나 비밀번호가 맞지 않습니다. 다시 확인해 주십시오.";
		}
		//< account is disabled
		else if(exception instanceof DisabledException) {
			errorMessage = "계정이 비활성화 되었습니다. 관리자에게 문의하세요.";
		}
		//< expired the credential
		else if(exception instanceof CredentialsExpiredException) {
			errorMessage = "비밀번호 유효기간이 만료 되었습니다. 관리자에게 문의하세요.";
		}
		else {
			errorMessage = "알수 없는 이유로 로그인에 실패하였습니다. 관리자에게 문의하세요.";
		}
		
		//< set attributes
		request.setAttribute("errorMessage", errorMessage);
		//< redirection
		request.getRequestDispatcher(DEFAULT_FAILURE_URL).forward(request, response);
	}
	
	////////////////////////////////////////////////////////////////////////////////
	//< private functions
}

 

실패한 Exception을 검사하여 해당 Exception에 맞는 에러 메시지를 로그인 페이지에 같이 전달하여 로그인 실패 이유를 사용자에게 노출시킵니다.

위에 구현된 Exception을 포함한 AuthenticationException의 종류는 다음과 같습니다.

 

  • UsernameNotFoundException : 계정 없음
  • BadCredentialsException : 비밀번호 불일치
  • AccountExpiredException : 계정만료
  • CredentialExpiredException : 비밀번호 만료
  • DisabledException : 계정 비활성화
  • LockedException : 계정잠김

마무리...

이상으로 로그인 성공 실패 시 추가작업을 할 수 있는 AuthenticationSuccessHandler, AuthenticationFailureHandler에 대해서 알아보았습니다.

다음 글에서는 Controller 및 404, 401등과 같이 HTTP 상태코드에 따른 에러페이지를 사용자화 하는 방법에 대해 알아보도록 하겠습니다.

 


U2ful은 입니다. @U2ful Corp.