목차

SimpleUrlAuthenticationFailureHandler 설명

Custom FailureHandler 구현

SecurityConfig 설정

LoginController 에러 메시지 파리미터 설정

Login 페이지 에러 메시지 출력

GitHub 링크


SimpleUrlAuthenticationFailureHandler 설명

Spring Security에서 사용자 인증 실패 시의 동작을 정의하는 클래스입니다.

일반적으로 사용자가 올바르지 않은 자격 증명(사용자 이름 또는 비밀번호)을 제공했을 때 호출되며, 그에 따른 처리를 담당합니다.


Custom FailureHandler 구현

CustomLoginFailureHandler.java

package com.example.login.config;

import java.io.IOException;
import java.net.URLEncoder;

import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

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

@Component
public class CustomLoginFailureHandler extends SimpleUrlAuthenticationFailureHandler  {

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

        if (exception instanceof BadCredentialsException) {
            errorMessage = "일치하는 회원정보가 없습니다.";
        } else if (exception instanceof InsufficientAuthenticationException) {
            errorMessage = "인증에 실패 하였습니다.";
        }

        errorMessage = URLEncoder.encode(errorMessage, "UTF-8");
        setDefaultFailureUrl("/login?error=true&exception=" + errorMessage);

        super.onAuthenticationFailure(request, response, exception); 
        
    }
    
}


SecurityConfig 설정

SecurityConfig.java

package com.example.login.config;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import lombok.RequiredArgsConstructor;
 
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    private final CustomLoginFailureHandler customLoginFailureHandler;
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        
        // csrf token
        http
            .csrf((csrf) -> csrf.disable());  // 비활성화
        
        // 인가
        http
            .authorizeHttpRequests((authorize) -> authorize
                    .requestMatchers("/", "/login", "/loginProc", "/join", "/joinProc").permitAll()  // 보안 검사 없이 접근을 허용 
                    .requestMatchers("/member/**").hasAnyRole("ADMIN", "USER")  // role - ADMIN, USER만 접근 허용
                    .requestMatchers("/admin").hasRole("ADMIN")  // role - ADMIN만 접근 허용
                    .anyRequest().authenticated()
            );
 
        // Custom Login
        http
            .formLogin((form) -> form
                    .loginPage("/login")
                    .usernameParameter("loginId")
                    .passwordParameter("loginPwd")
                    .loginProcessingUrl("/loginProc")
                    .failureHandler(customLoginFailureHandler) // failureHandler 추가
                    .permitAll()
            );
        
        // 세션 고정 보호
        http
            .sessionManagement((session) -> session
                    .sessionFixation((sessionFixation) -> sessionFixation
                            //.newSession()   // 로그인 시 세션 신규 생성
                            .changeSessionId() // 로그인 시 세션은 그대로 두고 세션 아이디만 변경
                    )
            );
        
        // 로그아웃
        http
            .logout((logout) -> logout.logoutUrl("/logout")
                    .logoutSuccessUrl("/")
            );
 
        return http.build();
    }
 
    /**
     * BCrypt 암호화
     * @return
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
  
}


LoginController 에러 메시지 파리미터 설정

LoginController.java

@GetMapping("/login")
public String login(@RequestParam(value = "error", required = false) String error
                    , @RequestParam(value = "exception", required = false) String exception
                    , HttpServletRequest request, Model model) {
        
    String prevPage     = request.getHeader("Referer");
    HttpSession session = request.getSession();

    if(prevPage != null && !prevPage.contains("/login")) {
        session.setAttribute("prevPage", prevPage); // 이전 페이지 세션으로 저장
    }
        
    model.addAttribute("error", error);  // 에러 여부
    model.addAttribute("exception", exception); // 에러 메시지
        
    return "login";
}


Login 페이지 에러 메시지 출력

Login.musatache

<!DOCTYPE html>
<html lang="ko"> 
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>로그인</title>
        <style>
            .container {
                width: 800px !important ;
            }
        </style>
    </head>
    <body>
        <div class="container">
            <form name="loginForm" method="post" action="/loginProc">
                <div class="row">
                    <div class="col-md-12 pt-5">
                        <div class="card mb-4">
                            <h5 class="card-header text-center">로그인</h5>
                            <div class="card-body">
                                <div class="mb-3 row">
                                    <label for="html5-text-input" class="col-md-2 col-form-label">아이디</label>
                                    <div class="col-md-10">
                                        <input class="form-control" type="text" id="loginId" name="loginId" />
                                    </div>
                                </div>
                                <div class="mb-3 row">
                                    <label for="html5-search-input" class="col-md-2 col-form-label">비밀번호</label>
                                    <div class="col-md-10">
                                        <input class="form-control" id="loginPwd" name="loginPwd" type="password" />
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
                {{#error}}
                <div class="row">
                    <div class="alert alert-warning">
                        <strong>{{exception}}</strong>
                    </div> 
                </div>
                {{/error}}
                <div class="row">
                    <div class="col-md-12 text-center">
                        <button type="submit" class="btn btn-outline-danger">로그인</button>
                        <a href="/" class="btn btn-outline-secondary">취소</a>
                    </div>
                </div>
            </form>
        </div>
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
    </body>
</html>


GitHub 링크

전체 소스 다운로드

©2024, DevDream