들어가며...
로그인 성공 또는 실패 시 상황에 맞게 좀 더 세부적으로 수행해야 할 작업들이 있을 수 있습니다.
예를 들면 로그인 실패 시 실패 원인을 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.
'Programming > Spring Security' 카테고리의 다른 글
[Spring Security][회원가입 및 로그인 예제 8/9] View 구현 (1) | 2020.03.31 |
---|---|
[Spring Security][회원가입 및 로그인 예제 7/9] Controller 구현 (0) | 2020.03.23 |
[Spring Security][회원가입 및 로그인 예제 5/9] Web Security Configuration 설정 (1) | 2020.03.23 |
[Spring Security][회원가입 및 로그인 예제 4/9] Service Layer 구현 (0) | 2020.03.19 |
[Spring Security][회원가입 및 로그인 예제 3/9] Repository 생성 (JPA : Query Mapping) (0) | 2020.03.13 |