본문 바로가기
Spring Boot

스프링 부트(Spring Boot) 게시판 - 로그인/로그아웃 & 로그인 세션(Login session) 체크 기능 구현하기 [Thymeleaf, MariaDB, IntelliJ, Gradle, MyBatis]

by 도뎡 2023. 5. 11.
반응형
  • 본 게시판 프로젝트는 단계별(step by step)로 진행되니, 이전 단계를 진행하시는 것을 권장드립니다.
  • DBMS 툴은 DBeaver를 이용하며, DB는 MariaDB를 이용합니다. (MariaDB 설치하기)
  • 화면 처리는 HTML5 기반의 자바 템플릿 엔진인 타임리프(Thymeleaf)를 사용합니다.

이전 글에서는 회원 테이블을 생성하고, 레이어 팝업을 이용해 회원가입 기능을 구현해 보았습니다. 이번에는 로그인과 로그아웃 기능을 구현하고, 인터셉터를 이용해서 로그인이 되어있지 않은 경우에는 게시판에 접근할 수 없도록 컨트롤해 보도록 하겠습니다.

 

1. MemberService - 로그인 메서드 추가하기

먼저 MemberService에 아래 메서드를 추가해 주세요.

    /**
     * 로그인
     * @param loginId - 로그인 ID
     * @param password - 비밀번호
     * @return 회원 상세정보
     */
    public MemberResponse login(final String loginId, final String password) {
        
        // 1. 회원 정보 및 비밀번호 조회
        MemberResponse member = findMemberByLoginId(loginId);
        String encodedPassword = (member == null) ? "" : member.getPassword();

        // 2. 회원 정보 및 비밀번호 체크
        if (member == null || passwordEncoder.matches(password, encodedPassword) == false) {
            return null;
        }

        // 3. 회원 응답 객체에서 비밀번호를 제거한 후 회원 정보 리턴
        member.clearPassword();
        return member;
    }

 

로직 번호 설명
1번 로그인 페이지에 입력한 아이디와 비밀번호를 전달받아 회원 정보를 조회합니다. encodedPassword는 회원 테이블에 암호화된 비밀번호인데요. member가 null인 경우에 member.getPassword( )를 실행하면 NPE가 발생하기 때문에 빈 문자열(" ")로 처리합니다.
2번 passwordEncoder의 matches( )로 사용자가 입력한 비밀번호(password)와 회원 테이블에 암호화된 비밀번호(encodedPassword)를 비교합니다. 두 조건 모두 false인 경우가 정상적인 케이스입니다.
3번 member.clearPassword( )로 암호화된 회원의 비밀번호를 초기화(" ") 한 후 회원 응답 객체를 리턴합니다.

 

2. MemberController - 로그인/로그아웃 메서드 추가하기

다음으로 MemberController에 아래 두 메서드를 추가해 주세요.

    // 로그인
    @PostMapping("/login")
    @ResponseBody
    public MemberResponse login(HttpServletRequest request) {

        // 1. 회원 정보 조회
        String loginId = request.getParameter("loginId");
        String password = request.getParameter("password");
        MemberResponse member = memberService.login(loginId, password);

        // 2. 세션에 회원 정보 저장 & 세션 유지 시간 설정
        if (member != null) {
            HttpSession session = request.getSession();
            session.setAttribute("loginMember", member);
            session.setMaxInactiveInterval(60 * 30);
        }
        
        return member;
    }

    // 로그아웃
    @PostMapping("/logout")
    public String logout(HttpSession session) {
        session.invalidate();
        return "redirect:/login.do";
    }

 

메서드 설명
login( ) 1. request 객체의 getParameter( )로 사용자가 로그인 페이지에 입력한 아이디와 비밀번호를 변수에 담아 회원 정보를 조회합니다.

2. MemberService의 login( )은 회원 정보가 알맞게 입력된 경우에만 member 객체를 리턴합니다. 로그인에 성공하면 member 객체를 loginMember라는 이름으로 세션에 저장한 후 로그인 유지 시간을 30분으로 설정합니다. session.setMaxInactiveInterval( )은 세션 타임아웃을 설정하는 메서드로 초를 기준으로 값을 전달해 주어야 합니다. (1800초 == 30분)
logout( ) session.invalidate( )로 세션을 무효화(초기화) 한 후 사용자를 로그인 페이지로 이동시킵니다.

 

3. 로그인(세션) 체크용 인터셉터 추가하기

1, 2번에서 로그인/로그아웃 처리는 끝났으니 로그인 상태를 체크할 인터셉터를 추가할 차례입니다. java 디렉터리에 아래 클래스를 추가해 주세요.

package com.study.interceptor;

import com.study.domain.member.MemberResponse;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class LoginCheckInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 1. 세션에서 회원 정보 조회
        HttpSession session = request.getSession();
        MemberResponse member = (MemberResponse) session.getAttribute("loginMember");

        // 2. 회원 정보 체크
        if (member == null || member.getDeleteYn() == true) {
            response.sendRedirect("/login.do");
            return false;
        }

        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

}

 

로직 번호 설명
1번 MemberController의 login( )에서 세션에 저장한 회원 정보를 조회합니다.
2번 member 객체가 null이면 로그인이 되어있지 않음을 의미합니다. 2번의 조건이 하나라도 true인 경우에는 사용자를 로그인 페이지로 이동시키는데요. 이를 통해 URL을 입력해서 강제로 게시판에 접근하는 사용자를 막을 수 있습니다.

 

4. 애플리케이션에 인터셉터 등록하기

이전에 처리한 LoggerInterceptor와 마찬가지로 앞에 추가한 인터셉터 클래스도 스프링이 인식할 수 있도록 Config 클래스에 등록해 주어야 합니다. WebMvcConfig를 다음과 같이 변경해 주세요.

package com.study.config;

import com.study.interceptor.LoggerInterceptor;
import com.study.interceptor.LoginCheckInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoggerInterceptor())
                .excludePathPatterns("/css/**", "/images/**", "/js/**");

        registry.addInterceptor(new LoginCheckInterceptor())
                .addPathPatterns("/**/*.do")
                .excludePathPatterns("/log*");
    }

}

 

메서드 설명
addInterceptors( ) 이전에 로그용 인터셉터를 처리하는 과정에서 구현한 오버라이드 메서드입니다. 애플리케이션 내의 모든 페이지(URI)에 접근할 때 LoginCheckInterceptor의 preHandle( )이 작동하는데요. excludePathPatterns( )를 이용해서 로그인 페이지와 로그인/로그아웃 URI는 인터셉터 실행에서 제외시킵니다.

만약 로그인/로그아웃 관련 URI를 호출했을 때 인터셉터가 작동하게 되면, LoginCheckInterceptor의 preHandle( )에 의해 계속해서 로그인 페이지를 호출하는 무한 루프에 빠지게 됩니다.

 

5. login.html - 로그인 함수 추가하기

다음으로 로그인 페이지에 MemberController의 login( )을 호출할 onload( )와 login( ) 함수를 추가해 주세요.

	// Enter 로그인 이벤트 바인딩
	window.onload = () => {
		document.querySelectorAll('#loginId, #password').forEach(element => {
			element.addEventListener('keyup', (e) => {
				if (e.keyCode === 13) {
					login();
				}
			})
		})
	}


	// 로그인
	function login() {

		const form = document.getElementById('loginForm');

		if ( !form.loginId.value || !form.password.value ) {
			alert('아이디와 비밀번호를 모두 입력해 주세요.');
			form.loginId.focus();
			return false;
		}

		$.ajax({
			url : '/login',
			type : 'POST',
			dataType : 'json',
			data : {
				loginId: form.loginId.value,
				password: form.password.value
			},
			async : false,
			success : function (response) {
				location.href = '/post/list.do';
			},
			error : function (request, status, error) {
				alert('아이디와 비밀번호를 확인해 주세요.');
			}
		})
	}

 

함수 설명
onload( ) 해당 함수 안의 로직은 로그인 페이지의 아이디와 패스워드 필드에서 엔터(Enter)를 입력했을 때 login( )을 호출할 수 있도록 이벤트를 바인딩하는 역할을 합니다.
login( ) MemberController의 login( )을 호출해서 로그인을 시도합니다. 로그인에 성공하면 게시글 리스트 페이지로 이동하고, 실패한 경우에는 error( )안의 alert 메시지를 보여줍니다.

이 함수는 로그인에 실패했을 때 에러가 발생하는데요, @ResponseBody가 선언된 컨트롤러의 메서드가 null을 리턴하면 Ajax 호출 시 에러가 발생하기 때문에 callApi( )를 사용하지 않고 Ajax 함수를 따로 선언해 주었습니다. 나중에 전역 예외처리를 배우는 과정에서 에러 메시지를 공통으로 컨트롤하는 방법도 설명드리겠습니다. 우선은 맛보기 정도로 생각해 주세요 :)

 

6. 로그인 테스트해 보기

이제 로그인되지 않은 상태에서 URL에 게시글 리스트 페이지 주소를 강제로 입력해서 접근해 보면 LoginCheckInterceptor의 2번 조건에 의해 로그인 페이지로 이동됩니다.

LoginCheckInterceptor - preHandle( ) 디버깅 모드

 

로그인 페이지

 

다음은 로그인에 실패한 경우입니다. 아이디 또는 비밀번호가 잘못 입력되면 아래와 같은 메시지를 볼 수 있습니다.

로그인에 실패한 경우

 

마지막으로 로그인에 성공한 경우입니다. 로그인 후에는 게시글 리스트 페이지로 이동됩니다.

로그인에 성공한 경우

 

7. body.html - 회원 이름 출력하기 & 로그아웃 폼 추가하기

마지막으로 body.html 상단의 header 태그를 다음과 같이 변경해 주세요.

    <header>
        <div class="head">
            <h1>게시판 프로젝트</h1>
            <div th:if="${session.loginMember != null}" class="top_menu">
                <div class="login_user"><strong><i class="far fa-user-circle"></i> [[ ${session.loginMember.name} ]]</strong>님 반갑습니다.</div>
                <div class="logout">
                    <form action="/logout" method="post">
                        <button type="submit"><span class="skip_info">로그아웃</span><i class="fas fa-sign-out-alt"></i></button>
                    </form>
                </div>
            </div>
        </div>
    </header>

 

태그 설명
div.top_menu 로그인된 경우에만 출력되는 태그입니다. 아래 strong 태그에서는 타임리프 문법( [[ ${session.loginMember.name} ]] )을 이용해서 세션에 저장된 회원의 이름을 출력합니다.
form 로그아웃 action을 호출하는 폼 태그입니다. MemberController의 logout을 호출해 세션을 초기화합니다.

 

다시 로그인해 보면 페이지 우측 상단에 loginMember의 name에 해당되는 회원명이 출력되는 걸 확인하실 수 있습니다.

페이지 우측 상단 - 세션에 저장된 회원명 출력

 

마치며

여기까지 회원 기능 구현이 모두 완료되었습니다. 다음 글부터는 파일 업로드/다운로드 기능을 구현해 볼 건데요. 업로드 > 다운로드 순으로 총 세 개의 글로 나누어 처리하게 됩니다. 파일 처리는 게시글, 댓글, 회원보다는 복잡하다 느끼실 수 있으나, 최대한 깨끗하고 심플하게 로직을 작성해서 쉽게 설명드려 보도록 하겠습니다.

오늘도 방문해 주신 여러분께 감사의 말씀을 전합니다. 좋은 하루 보내세요 :)

Board.zip
0.90MB

반응형

댓글