[JAVA PROJECT]
JAVA Spring Boot 프로젝트- 1. 스프링시큐리티를 이용한 회원가입 및 로그인 구현하기(2)
에브리코더
2023. 12. 23. 23:20
3. 로그인 / 로그아웃 기능 구현하기
UserDetailsServiceImpl 생성
UserDetailsServiceImpl 클래스는 Spring Framework의 보안 기능과 연동되어 사용자 인증 및 권한 부여에 사용됩니다.
사용자인증과정에서 사용자의 자격을 검증하고 권한을 부여하는데 사용됩니다. UserRepository를 사용하여 데이터베이스에서 사용자 정보를 조회하고, 조회된 정보를 바탕으로 UserDetails 객체를 생성하여 반환합니다.
package com.wine.demo.service;
import java.util.ArrayList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.wine.demo.model.User;
import com.wine.demo.repository.UserRepository;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("id에 해당하는 유저를 찾을수 없습니다 : " + username);
}
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), new ArrayList<>());
}
}
- loadUserByUsername() 사용자의 이름을 기반으로 사용자의 정보를 검색하고 UserDetails 객체로 반환합니다. 만약 사용자를 찾을수 없다면 UsernameNotFoundException을 발생시킵니다.
Handler 생성
CustomAuthenticationSuccessHandler
로그인 성공시 호출되는 핸들러입니다. 사용자가 성공적으로 로그인하면 이 핸들러가 실행됩니다.
package com.wine.demo.handler;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
public CustomAuthenticationSuccessHandler(String defaultTargetUrl) {
setDefaultTargetUrl(defaultTargetUrl);
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
// 세션에 사용자 이름 설정
request.getSession().setAttribute("username", authentication.getName());
// 세션에서 이전 페이지 URL 가져오기
String prevPage = (String) request.getSession().getAttribute("prevPage");
if (prevPage != null) {
request.getSession().removeAttribute("prevPage"); // 사용 후 세션에서 삭제
}
// 이전 페이지가 없는 경우 홈페이지로 리디렉션, 있으면 해당 페이지로 리디렉션
response.sendRedirect(prevPage != null ? prevPage : getDefaultTargetUrl());
}
}
UserLoginFailHandler
로그인 실패 시 호출되는 핸들러입니다. 사용자가 로그인에 실패하면 이 핸들러가 실행되어 사용자에게 오류 메시지를 표시합니다.
package com.wine.demo.handler;
import java.io.IOException;
import java.net.URLEncoder;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
@Component
public class UserLoginFailHandler 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 InternalAuthenticationServiceException) {
errorMessage = "내부적으로 발생한 시스템 문제로 인해 요청을 처리할 수 없습니다. 관리자에게 문의하세요.";
} else if (exception instanceof UsernameNotFoundException) {
errorMessage = "계정이 존재하지 않습니다. 회원가입 진행 후 로그인 해주세요.";
} else if (exception instanceof AuthenticationCredentialsNotFoundException) {
errorMessage = "인증 요청이 거부되었습니다. 관리자에게 문의하세요.";
} else {
errorMessage = "알 수 없는 이유로 로그인에 실패하였습니다 관리자에게 문의하세요.";
}
errorMessage = URLEncoder.encode(errorMessage, "UTF-8");
setDefaultFailureUrl("/login?error=true&exception=" + errorMessage);
super.onAuthenticationFailure(request, response, exception);
}
}
CustomLogoutSuccessHandler
로그아웃 성공 시 호출되는 핸들러입니다. 사용자가 로그아웃하면 이 핸들러가 실행됩니다.
package com.wine.demo.handler;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.sendRedirect("/login");
}
}
3. SecurityConfig 설정
CustomAuthenticationSuccessHandler, UserLoginFailHandler, CustomLogoutSuccessHandler 등을 통해 로그인 성공, 실패, 로그아웃 시의 동작을 설정합니다.
package com.wine.demo.config;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private UserLoginFailHandler userLoginFailHandler;
@Bean
public AuthenticationSuccessHandler successHandler() {
return new CustomAuthenticationSuccessHandler(
SecurityConstants.DEFAULT_SUCCESS_URL
);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
.formLogin()
.loginPage("/login")
.successHandler(customAuthenticationSuccessHandler())
.failureHandler(userLoginFailHandler)
.and()
.logout()
.logoutUrl("/logout")
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessHandler(customLogoutSuccessHandler())
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.clearAuthentication(true)
.permitAll();
}
@Override
public void configure(WebSecurity web) throws Exception {
}
@Bean
public CustomLogoutSuccessHandler customLogoutSuccessHandler() {
return new CustomLogoutSuccessHandler();
}
@Bean
public UserLoginFailHandler userLoginFailHandler() {
return new UserLoginFailHandler();
}
}
UserService 수정
package com.wine.demo.service;
import
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private VerificationCodeRepository verificationCodeRepository;
@Autowired
private JavaMailSender mailSender;
@Autowired
private AuthenticationManager authenticationManager;
// 로그인 처리를 담당하는 메서드 사용자 이름과 비밀번호로 로그인을 시도하고, 인증 결과를 반환합니다.
public boolean loginUser(String username, String password) {
try {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password));
return authentication.isAuthenticated();
} catch (UsernameNotFoundException | BadCredentialsException ex) {
return false;
}
}
// ..기존 회원가입 메서드
UserController 수정
package com.wine.demo.controller;
@Controller
public class UserController {
// 로그인페이지
@GetMapping("/login")
public String showLoginPage(HttpServletRequest request, HttpSession session) {
String referrer = request.getHeader("Referer"); // 이전에 방문한 페이지의 url을 가져옴
if (referrer != null && !referrer.contains("/login")) {
session.setAttribute("prevPage", referrer); // 이전 페이지의 url을 세션에 저장
}
return "login/login";
}
// 로그인 요청을 처리
@PostMapping("/login")
public String loginUser(@RequestParam String username, @RequestParam String password, HttpSession session, Model model) {
if (username == null || password == null) {
model.addAttribute("error", "사용자 이름 또는 비밀번호가 누락되었습니다.");//입력누락에 대한 간단한 검증만 수행하고 로그인시도및 실패처리는 handler가 담당
return "login";
}
if (userService.loginUser(username, password)) {
session.setAttribute("username", username); //로그인정보 세션에 저장
String prevPage = (String) session.getAttribute("prevPage"); // 이전에 방문한 페이지로 리다이렉트(session의 prevpage에서 이전페이지정보를 가져옴)
session.removeAttribute("prevPage"); //페이지url데이터 세션에서 삭제
return "redirect:" + (prevPage != null ? prevPage : "/"); // 이전 페이지가 null이 아니면 해당페이지로 null이면 홈페이지로 리다이렉트
} else {
model.addAttribute("error", "잘못된 사용자 이름 또는 비밀번호입니다.");
return "login";
}
}
//..기존 회원가입관련 메서드
로그인 페이지 생성 login.html
회원가입과 마찬가지로 login bootstrap을 검색하시면 다양한 폼을 찾을수 있습니다.
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>KOWASA 로그인</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link href="https://fonts.googleapis.com/css?family=Lato:300,400,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" href="css/style_login.css">
</head>
<body>
<section class="ftco-section">
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 text-center mb-5">
<h2 class="heading-section"></h2>
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-12 col-lg-10">
<div class="wrap d-md-flex">
<div class="img" style="background-image: url(img/bg-1.jpg);">
</div>
<div class="login-wrap p-4 p-md-5">
<div class="d-flex">
<div class="w-100">
<div style="display: flex; align-items: center; flex-direction: row;">
<a href="/"><img src="img/winelogo.jpg" alt="Image" width="50" height="50"></a>
<div style="margin-left: 10px;">
<a href="/" class="m-0" style="color: #D81324; font-size:24px;" >KOWASA</a>
</div>
</div>
<h3 class="mb-4" style="margin-top: 20px;">로그인</h3>
</div>
<div class="w-100">
<p class="social-media d-flex justify-content-end">
<a href="/oauth2/authorization/google" class="social-icon d-flex align-items-center justify-content-center"><span class="fa fa-google"></span></a>
</p>
</div>
</div>
<form id="loginForm" action="/login" method="post" class="signin-form">
<div class="form-group mb-3">
<label class="label" for="username">아이디</label>
<label class="label" style="float: right;"><input type="radio" id="adminUser" name="userType" value="admin">관리자</label>
<label class="label" style="float: right;"><input type="radio" id="generalUser" name="userType" value="general" checked>일반회원</label>
<input type="text" name="username" id="username" class="form-control" placeholder="아이디를 입력해주세요" required>
</div>
<div class="form-group mb-3">
<label class="label" for="password">비밀번호</label>
<input type="password" name="password" id="password" class="form-control" placeholder="비밀번호를 입력해주세요" required>
</div>
<div class="form-group">
<button type="submit" class="form-control btn btn-primary rounded submit px-3">로그인</button>
</div>
<div th:if="${param.error}">
<p class="error-message" th:text="${param.exception}"></p>
</div>
<div class="form-group d-md-flex">
<div class="w-50 text-left">
<label class="checkbox-wrap checkbox-primary mb-0">아이디 기억하기
<input type="checkbox" id="remember-id">
<span class="checkmark"></span>
</label>
</div>
<div class="w-50 text-md-right">
<a href="/findIdPw">아이디/비밀번호 찾기</a>
</div>
</div>
</form>
<p class="text-center">아직 회원이 아니신가요? <a href="/register">회원가입</a></p>
</div>
</div>
</div>
</div>
</div>
</section>
<script src="/js/jquery_login.min.js"></script>
<script src="/js/popper_login.js"></script>
<script src="/js/bootstrap_login.min.js"></script>
<script src="/js/main_login.js"></script>
<script src="/js/remember_id.js"></script>
</body>
</html>