본문 바로가기

Dev/Spring Boot

스프링 시큐리티 로그인 인증 그 후...

스프링 시큐리티를 공부하는 과정에서 공부하고 이해한 내용을 바탕으로 풀어보려고 한다.


현재 개인적으로 프로젝트를 진행 도중 회원가입을 하는 과정에서 가입하려는 이메일로 인증 과정을 거치게 하였다.

여기서 문제는 가입하려는 이메일로 인증을 다 받기도 전에 디비에 먼저 데이터를 넣어주다보니 로그인이 되버리는 현상이 생겼다.

시큐리티에서 로그인할 때 이메일 인증이 안되면 인증이 거부되어 튕기게 할 수 없을까?? 라는 의문점이 생겼다.

구글을 뒤적뒤적.. 닥치는데로 찾고 정리하고 공부하기 시작!

방법은 있었다! 지난번에 스프링 시큐리티에 관해서 정리를 한 적이 있는데 거기에 이어서 설명하고자 한다.

 

지난글을 보려면 여기를 확인하면 된다.

2020/11/19 - [Dev/Spring Boot] - 스프링 시큐리티 적용 (CSRF)

 

스프링 시큐리티 적용 (CSRF)

스프링 시큐리티(Spring Security)에 대해서 공부한 내용을 정리하며 진행 이해한 내용을 바탕으로 풀어서 정리하였다. 먼저 스프링 시큐리티라고 하면 제일먼저 등장하는 흐름도이다. 아래는 스프

okdolmin.tistory.com


스프링 시큐리티는 기본적으로 SecurityFilterChain 들을 통해 여러 필터들을 거쳐간다.

로그인시 거쳐가는 필터는 UsernamePasswordAuthenticationFilter 이녀석을 통해 인증을 진행한다.

 

인증을 진행할 때 사용자 편의에 맞춰서 두개의 Handler를 제공하는데 AuthenticationFailureHandler와 AuthenticationSuccessHandler가 그것들이다.


AuthenticationFailureHandler : 이름에서 유추할 수 있듯이 로그인 인증하는 과정에서 실패할 경우의 동작

                                             ex) 로그인 5회 실패시 사용자 계정 잠금, 예외를 통해 메시지를 뿌리기 등의 부가적인 작업

 

AuthenticationSuccessHandler : 이름에서 유추할 수 있듯이 로그인 인증하는 과정에서 성공한 이후의 동작

                                             ex) 로그인 성공시 수행할 부가적인 작업

 

아래 직접 만들어보고 있는 프로젝트의 소스를 보면서 자세히 살펴보자!


시큐리티 설정파일(SecurityConfig.java) 파일 일부분

    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http
            .csrf().disable()
            .authorizeRequests()
            // 페이지 권한 설정
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/","/user/signup", "/user/login", "/user/sendSignUpEmail", "/user/signUpConfirm","/study/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginPage("/user/login")
            .defaultSuccessUrl("/")
            .failureHandler(failureHandler()) // 이부분이 실패했을때 이 핸들러를 타겠다 라는 뜻
//            .successHandler(successHandler()) // 이부분이 성공했을때 이 핸들러를 타겠다 라는 뜻
            .permitAll()
            .and() // 로그아웃 설정
            .logout()
            .logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
            .logoutSuccessUrl("/")
            .invalidateHttpSession(true)
            .deleteCookies("JSESSIONID")
            .permitAll();
        http.sessionManagement()
                .maximumSessions(1)
                .maxSessionsPreventsLogin(false);

    }

	// 빈주입을 통해 AuthenticationFailureHandler를 탈경우에 내가 생성한 LoginFailHandler에서
    // 추가적인 행동을 하겠다 라는 뜻의 선언이다.
    @Bean
    public AuthenticationFailureHandler failureHandler() {
        return new LoginFailHandler();
    }

 

필자는 별도의 예외처리를 통해 예외메시지를 지정하고 그 메시지를 화면을 loginFail이라는 뷰 화면에서 알림메시지로 던져준다.

  • LoginFailHandler는 AuthenticationFailureHandler를 상속받게 되는데 이렇게되면 onAuthenticationFailure를 오버라이딩 해줘야 한다!
  • 오버라이딩한 메서드는 AuthenticationException으로 하여 인증 실패시 제공되는 예외들이 있고, 각 예외마다 메시지를 던질수 있고 아래와같이 별도로 메시지를 변경해서 던질 수 있다.
  • 대표적으로는 아래에서 선언한 것처럼 BadCredentialsException, DisabledException, LockedException 등이 있다.
  • 아래의 소스에서 캡쳐한 추가 이미지를 참고하자.

AuthenticationException에서 제공하는 예외들

LoginFailHandler.java 파일

package com.bootproj.pmcweb.Network.Request;

import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import lombok.extern.slf4j.XSlf4j;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import javax.security.auth.login.AccountException;
import javax.security.auth.login.AccountExpiredException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Getter
@Setter
@Slf4j
public class LoginFailHandler implements AuthenticationFailureHandler {
    private final String DEFAULT_FAILURE_URL = "/user/loginFail";

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        String errorMsg = null;

        if(exception instanceof BadCredentialsException){
            errorMsg = "아이디나 비밀번호가 맞지 않습니다. 다시 확인해 주세요.";
        }else if(exception instanceof DisabledException) {
            errorMsg = "계정이 비활성화 되었습니다. 관리자에게 문의하세요.";
        }else if(exception instanceof LockedException){
            log.info("이메일이 인증되지 않았습니다. 이메일을 확인해 주세요.");
            errorMsg = "이메일이 인증되지 않았습니다. 이메일을 확인해 주세요.";
        }else{
            errorMsg = "알수없는 이유로 로그인에 실패하였습니다.";
        }
        request.setAttribute("errorMsg", errorMsg);
        request.getRequestDispatcher(DEFAULT_FAILURE_URL).forward(request, response);
    }
}

 

 


로그인을 하게될때의 과정을 이해한데로 다시한번 종합적으로 정리해보자면!

  1. 로그인을 하게되면 스프링 시큐리티에서 인증절차를 받기 위해 UserDetailsService를 상속받고 있는 서비스의 loadUserByUsername를 찾아간다.
  2. 인증하는 과정에서 예외가 발생하게 되면, 기본적으로 시큐리티에서 Fail시 처리하는 로직이 수행되지만, 필자는 위에서 별도로 만든 Handler를 통해 예외메시지를 던져주게 구현한 것이다.
  3. 이상 끝!