[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, UserLoginFailHandlerCustomLogoutSuccessHandler 등을 통해 로그인 성공, 실패, 로그아웃 시의 동작을 설정합니다.

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>

 

css와 js부분은 분량이 길어질거같아 생략했습니다..