SimpleUrlAuthenticationFailureHandler 설명
Custom FailureHandler 구현
SecurityConfig 설정
LoginController 에러 메시지 파리미터 설정
Login 페이지 에러 메시지 출력
GitHub 링크
Spring Security에서 사용자 인증 실패 시의 동작을 정의하는 클래스입니다.
일반적으로 사용자가 올바르지 않은 자격 증명(사용자 이름 또는 비밀번호)을 제공했을 때 호출되며, 그에 따른 처리를 담당합니다.
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.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.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.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>