Spring boot/Spring Security

02. SpringSecurity - AuthenticationFailureHandler 로그인 실패시 실패 메시지 띄우기

akatjd 2021. 6. 20. 22:43

01) SecurityConfiguration 설정

 

- configure(HttpSecurity http) 메서드에 http.formLogin().failureHandler(loginFailHandler) 설정해줘야 함.

 

package com.kjc.workplus.configuration;

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.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.kjc.workplus.login.handler.LoginFailHandler;
import com.kjc.workplus.login.service.MemberService;

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
	
	@Autowired
	private MemberService memberService;
	
	@Autowired
	private LoginFailHandler loginFailHandler;

	@Bean
	public PasswordEncoder passwordEncoder() {
		
		return new BCryptPasswordEncoder();
		
	}
	
	@Override
	public void configure(WebSecurity web) throws Exception {
		
		web.ignoring().antMatchers("/css/**", "/js/**", "/img/**", "/lib/**");
		
	}
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		
		http.authorizeRequests()
		        .antMatchers("/workplus/member/**").hasRole("MEMBER")
		        .antMatchers("/workplus/admin/**").hasRole("ADMIN")
		        .antMatchers("/workplus/**").permitAll();

		http.formLogin()
		        .loginPage("/workplus/login.do")
		        .defaultSuccessUrl("/workplus/main.do")
		        .failureHandler(loginFailHandler)
		        .permitAll();
		
		http.logout()
		        .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
		        .logoutSuccessUrl("/workplus/main.do")
		        .invalidateHttpSession(true);
		
		http.exceptionHandling()
		        .accessDeniedPage("/workplus/denied.do");
		
	}
	
	@Override
	public void configure(AuthenticationManagerBuilder auth) throws Exception {
		
		auth.userDetailsService(memberService).passwordEncoder(passwordEncoder());
		
	}
}

 

- LoginFailHandler를 작성해줘야 함. (Service에 작성)

 

package com.kjc.workplus.login.handler;

import java.io.IOException;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Service;

@Service("loginFailHandler")
public class LoginFailHandler implements AuthenticationFailureHandler{
	
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
		
		Logger log = LoggerFactory.getLogger(this.getClass());
		
		// TODO Auto-generated method stub
		if(exception instanceof AuthenticationServiceException) {
			request.setAttribute("LoginFailMessage", "죄송합니다. 시스템에 오류가 발생했습니다.");
		}
		else if(exception instanceof BadCredentialsException) {
			request.setAttribute("LoginFailMessage", "아이디 또는 비밀번호가 일치하지 않습니다.");
		}
		else if(exception instanceof DisabledException) {
			request.setAttribute("LoginFailMessage", "현재 사용할 수 없는 계정입니다.");
		}
		else if(exception instanceof LockedException) {
			request.setAttribute("LoginFailMessage", "현재 잠긴 계정입니다.");
		}
		else if(exception instanceof AccountExpiredException) {
			request.setAttribute("LoginFailMessage", "이미 만료된 계정입니다.");
		}
		else if(exception instanceof CredentialsExpiredException) {
			request.setAttribute("LoginFailMessage", "비밀번호가 만료된 계정입니다.");
		}
		else request.setAttribute("LoginFailMessage", "계정을 찾을 수 없습니다.");
		RequestDispatcher dispatcher = request.getRequestDispatcher("/workplus/login.do");
		System.out.println(request.getAttribute("LoginFailMessage"));
		dispatcher.forward(request, response);
		
	}
}

 

Fail Handler가 받는 예외 디폴트 Provider가 리턴하는 결과 (커스터마이징 가능)
AuthenticationServiceException - null 값을 리턴
BadCredentailException - UsernameNotFoundException 예외를 throw
- UserDetails 객체를 리턴했으나, 아이디 또는 비밀번호가 틀림
LockedException - UserDetails 객체의 isAccountNonLocked() 메서드의 리턴값이 false
DisabledException - UserDetails 객체의 isEnabled() 메서드의 리턴값이 false
AccountExpiedException - UserDetails 객체의 isAccountNonExpired() 메서드의 리턴값이 false
CredentailExpiredException - UserDetails 객체의 isCredentialsNonExpired() 메서드의 리턴값이 false

 

02) View 처리

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" 
	  xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" 
	  xmlns:sec="http://www.w3.org/1999/xhtml"
	  xmlns="http://www.w3.org/1999/xhtml"
	  layout:decorator="board/layout/basic"
	  layout:decorate="~{layout/default_layout}">
	  
    <head>
		<meta charset="UTF-8">
		<title>로그인</title>
		<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">
		<script src="/webjars/bootstrap/3.3.4/dist/js/bootstrap.min.js"></script>
		<th:block layout:fragment="head">
			<!--/* css */-->
			<link th:href="@{/css/common.css}" rel="stylesheet" />
		</th:block>
	</head>
    <body>
        <h1>로그인</h1>
        <div th:if="${LoginFailMessage!=null}">
	        <p th:text="${LoginFailMessage}" class="alert alert-danger"></p>
	    </div>
        <hr>
        <form th:action="@{/workplus/login.do}" method="post">
        	<input th:type="hidden" th:value="secret" name="secret_key">
            <input type="text" name="username" placeholder="account를 입력해주세요.">
            <input type="password" name="password" placeholder="password를 입력해주세요.">
            <button type="submit">로그인</button>
        </form>
    </body>
</html>

 

<div th:if="${LoginFailMessage!=null}">
	<p th:text="${LoginFailMessage}" class="alert alert-danger"></p>
</div>

 

로그인시 에러 발생하면 LoginFailMessage 애트리뷰트가 포워딩 되며 해당 오류 알러트를 띄어줌.