Blog 게시판 만들기 (3) - 로그인 / 로그아웃 기능 만들기

bethe·2022년 9월 6일
0

Springboot

목록 보기
34/46

📝 로그인 기능 만들기

기억하기 : select에서 login만 POST요청이다
기본적으로 select는 GET요청이다.
그런데 login은 POST(body)로 데이터를 받아 같은 데이터가 있는지 확인하는 select다. (찾아지면 login, null이면 login 실패)

POST를 사용하는 이유 : login 정보는 너무 중요한 정보이므로 주소에 담기면 안 되기 때문


1. Mapper

	<select id="login" resultType="site.metacoding.red.domain.users.Users">
		SELECT * FROM users WHERE username=#{username} AND password=#{password}
	</select>

2. loginForm.jsp

  • form태그의 action과 method(post), key값 (name="") 설정하기
<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>

<%@ include file="../layout/header.jsp"%>

<div class="container">
	<form action="/login" method="post">
		<div class="mb-3 mt-3">
			<input
				type="text" class="form-control"
				placeholder="Enter username" name="username">
		</div>
		<div class="mb-3">
			<input
				type="password" class="form-control" 
				placeholder="Enter password" name="password">
		</div>
		<button type="submit" class="btn btn-primary">로그인</button>
	</form>
</div>

<%@ include file="../layout/footer.jsp"%>

3. loginDTO

package site.metacoding.red.web.dto.request.users;

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public class LoginDto {
	private String username;
	private String password;
}

4. Controller - 1차 : return을 메인 페이지로 지정

	@PostMapping("/login")
	public String login(LoginDto loginDto) {
		return "redirect:/";
	}

5. DAO에 login 메서드 생성

package site.metacoding.red.domain.users;

import java.util.List;

import site.metacoding.red.web.dto.request.users.JoinDto;
import site.metacoding.red.web.dto.request.users.LoginDto;

public interface UsersDao {
	public Users login(LoginDto loginDto);
    //loginDto의 where 키값으로 Users 타입의 데이터를 받는다
	public void insert(JoinDto joinDto);
	public Users findById(Integer id);
	public List<Users> findAll();
	public void update(Users users); // DTO 생각해보기
	public void delete(Integer id);
}

6. Controller - 2차 : 조건문을 이용해 login 성공 or 실패시 return값 설정

	@PostMapping("/login")
	public String login(LoginDto loginDto) {
		Users usersPS = usersDao.login(loginDto);
		//DB에서 받은 데이터면 PS를 붙여주자.
		//그래야 DB에서 받은건지, 사용자에게 받은건지 구분이 된다.
		if(usersPS !=null){ // 인증됨
			return"redirect:/";
		}else { // 인증안됨
		return "redirect:/loginForm";
	}

7. Controller - 3차 : 로그인 지속을 위한 HttpSession 설정

로그인이 지속되려면 로그인이 되었다는 정보를 저장해야 하는데, request는 응답시 데이터가 날아가므로 저장하지 못한다. 👉 session이라는 server의 메모리 영역에 저장 (톰캣이 들고있음)

(1) request객체로 session 주소 얻기

그런데 세션에 접근하려면 request객체가 필요하다. request 객체는 DS가 가지고 있다. 파라미터에 HttpServletRequest를 넣으면 DS가 request를 준다.(이유 : reflection)

	@PostMapping("/login")
	public String login(LoginDto loginDto, HttpServletRequest request) {
		Users usersPS = usersDao.login(loginDto);
		//DB에서 받은 데이터면 PS를 붙여주자.
		//그래야 DB에서 받은건지, 사용자에게 받은건지 구분이 된다.
		if(usersPS !=null){ // 인증됨
			HttpSession session = request.getSession();
			return"redirect:/";
		}else { // 인증안됨
		return "redirect:/loginForm";
		}
	}

HttpSession session = request.getSession();에서 session이라는 레퍼런스 주소(변수)는 세션 메모리에 접근하는 주소가 된다.

(2) HttpSession 의존성 주입(DI)와 키값 설정하기

Dispatcher Sevlet이 제공해주는 HttpSession은 IOC 컨테이너에 하나의 오브젝트로 있다. 왜냐하면 IOC 컨테이너는 타입별로 하나씩만 띄울 수 있기 때문이다.

HttpSession은 JSESSIONID라는 이름의 세션을 자동으로 생성한다. 하나의 세션에는 여러 값을 저장할 수 있고, session.setAttribute()로 데이터를 저장할 수 있다.

  • 주의할 점
    사실 password를 세션에 저장하면 안 된다. 실제로는 password를 해시코드로 바꿔서 암호화 하는 등의 방법을 사용한다.

session.setAttribute("키값", 받을데이터);
이 때 키값은 주로 'principal' 을 사용한다. 인증→인가 과정에서 접근 주체=인정된 유저를 Principal이라고 부른다. 키값은 무조건 String 타입이다.

📌 Spring은 HttpSession을 IOC 컨테이너에 서버 시작시 띄워 놓는다.
👉 따라서 HttpSession session = request.getSession(); 과정 필요 없이 DI로 HttpSession httpSession 생성자 주입만 하면 HttpSession을 사용할 수 있다.

🌳→🌿 HttpSession도 Object가 부모인데 왜 session을 Object<>(제네릭)으로 받지 못할까?
: 제네릭은 new할 때 사용 가능한데(예시 : List <Integer> list = new ArrayList <>();) HttpSession은 Spring Server가 시작될 때 이미 new 되어 있기 때문

package site.metacoding.red.web;

import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import lombok.RequiredArgsConstructor;
import site.metacoding.red.domain.users.Users;
import site.metacoding.red.domain.users.UsersDao;
import site.metacoding.red.web.dto.request.users.JoinDto;
import site.metacoding.red.web.dto.request.users.LoginDto;

@RequiredArgsConstructor
@Controller
public class UsersController {
	//회원가입, 로그인 등 인증에 필요한 주소는
	//주소에 Entity(table 명)을 붙이지 않는 경우가 많다.	
	
	private final HttpSession session; // 스프링이 서버시작시에 IOC 컨테이너에 띄움
	private final UsersDao usersDao;
	
	@GetMapping("/joinForm")
	public String joinForm() {
		return "users/joinForm";
	}
	
	@PostMapping("/join")
	public String join(JoinDto joinDto) {
		usersDao.insert(joinDto);
		return "redirect:/loginForm";
	}
	
	@GetMapping("/loginForm")
	public String loginForm() {
		return "users/loginForm";
	}
	
	@PostMapping("/login")
	public String login(LoginDto loginDto) { //HttpServletRequest 삭제
		Users usersPS = usersDao.login(loginDto);
		//DB에서 받은 데이터면 PS를 붙여주자.
		//그래야 DB에서 받은건지, 사용자에게 받은건지 구분이 된다.
		if(usersPS !=null){ // 인증됨
			session.setAttribute("principal", usersPS); //이때 키값은 무조건 String이다.
			//인증->인가에서 접근 주체=인정된 유저를 Principal이라고 함
			return"redirect:/";
		}else { // 인증안됨
		return "redirect:/loginForm";
		}
	}

}

(3) session 저장값 확인 테스트

Session에 principal 값이 있으면 로그인이 된 것이고 없으면 로그인이 안 된 것이다. 테스트해보자. EL표현식은 request와 session영역에 있는 값을 가져올 수 있으므로 jsp파일에서 테스트 해보자.

<body><h1>${sessionScope.principal.username}</h1>을 적어보자.
세션에 User object가 저장되었으므로 username이 나타나면 로그인이 잘 된 것이다. (username ssar로 로그인)


무슨 페이지로 이동하더라도 ssar이 출력되는 점을 보아 세션에 값이 저장되어 있음을 알 수 있다.

🤔 만약 request도 principal이라는 값을 들고 있다면?
request와 session 모두 동일한 값이 있을 경우 session만 표현하여 구분해준다면 충돌이 나지 않는다. (=request는 값 앞에 requestScope를 적지 않아도 된다.)
${sessionScope.principal}
${requestScope.princial}${principal}


8. login 상태에 따라 보일 header 조건문 설정

조건문을 사용하여 로그아웃 상태일 때 보일 header와 로그인 상태일 때 보일 header를 설정한다. Mybatis문법에서 if조건문은 else가 없으므로 choose-when otherwise 조건문을 사용해야 한다.

  • principal의 상태가 null이거나 공백(" ")일 때의 조건문은 EL표현식의 empty를 사용하면 된다.

${empty principal} 에서 empty란?
✍️ principal이 null 이거나 ""(공백)일 때를 의미한다.

[header.jsp 코드]

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Blog</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link
	href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
	rel="stylesheet">
<script
	src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body>

	<nav class="navbar navbar-expand-sm bg-dark navbar-dark">
		<div class="container-fluid">
			<a class="navbar-brand" href="/boards">Blog</a>
			<button class="navbar-toggler" type="button"
				data-bs-toggle="collapse" data-bs-target="#collapsibleNavbar">
				<span class="navbar-toggler-icon"></span>
			</button>
			<div class="collapse navbar-collapse" id="collapsibleNavbar">
				<ul class="navbar-nav">
					<c:choose>
						<c:when test="${empty principal}">
							<li class="nav-item"><a class="nav-link" href="/loginForm">로그인</a></li>
							<li class="nav-item"><a class="nav-link" href="/joinForm">회원가입</a></li>
						</c:when>
						<c:otherwise>
							<li class="nav-item"><a class="nav-link" href="/boards/writeForm">글쓰기</a></li>
							
						</c:otherwise>
					</c:choose>
				</ul>
			</div>
		</div>
	</nav>



📖 Session

세션의 동작순서

  1. 클라이언트가 페이지 요청
  2. 서버가 접근한 클라이언트의 쿠키를 확인하여 해당 session-id를 보냈는지 확인
  3. session-id가 없다면 서버가 session-id를 생성해 서버에 저장 및 클라이언트에게 돌려줌
  4. 서버에서 클라이언트로 돌려준 session-id는 클라이언트 브라우저의 쿠키(이름: JSESSIONID)에 저장
  5. 이후 클라이언트의 요청시마다, 브라우저는 httpheader에 cookie값을 들고 감. (Http 프로토콜)
  6. 서버는 자동으로 session-id를 확인하고(Http 프로토콜) 클라이언트의 세션 영역을 찾아줌
  7. 각 사용자마다 이렇게 session-id(session key)가 만들어지고, 각자의 key값을 찾아가며 로그인이 유지된다.

🌳→🌿 인증과 인가
select로 DB의 데이터를 찾았다고 해서 로그인이 된 것은 아니다. 이 사람이 username과 password가 정상이라는 인증이 되었을 뿐이다.

  • 인증이 된 뒤 session에 session-id가 만들어져야 페이지의 기능을 이용할 수 있는 권한이 생긴다. 이를 인가라고 한다. session-id에는 User의 정보가 담긴 Object가 담겨 있다.

🤔 Session-id에 User object를 저장했기에 로그인이 되었다?
😄 저장할 때의 key값이 있으면 login이 되었다고 보기로 한 것 (header.jsp의 조건문으로 설정함)


✍️ Http 특징 : 여전히 Stateless한 Http

Http는 Stateless의 특징을 가진다. 그런데 세션 영역에 세션 키값(principal)이 있어 다른 주소로 이동해도 로그인이 풀리지 않아 Stateful한 것처럼 보인다.

그러나 실제로 Stateful하다고 말하기 위해서는 서버가 소켓으로 계속 연결되어 있어야 한다.
Http는 다른 주소로 갈 때마다 Controller를 작동시키면서 데이터를 새로 불러오고(F5) 이 과정에서 브라우저의 세션 키값이 session에 있는 키값과 일치하는지 확인하는 session 인증이 일어날 뿐이다.


로그인을 유지하지 못하게 하는 방법 (로그아웃 하는 방법)

  1. 브라우저를 끄면 쿠키의 session key가 사라짐 ← session에는 여전히 값이 있음
    ⇒ 쿠키의 session key가 없기 때문에 세션에서 principal을 찾지 못한다.

  2. 일정 시간 이상 사용하지 않으면(보통 30분) sessionn에서 session key값을 삭제함 (web.xml에서 설정)

  3. 강제로 서버에서 session을 삭제하기


🌿→🌳 톰캣이 사용하는 메모리 영역
application : 서버가 꺼질 때까지 유지됨
session : 계속 유지되다 사용자가 브라우저를 끄거나 강제로 종료하면 사라짐
request : 요청하고 응답이 끝나면 사라짐
page


쿠키 확인해보기

F12-Application-Cookies를 들어가면 제일 처음 서버에게 받은 JSESSIONID값이 있음을 알 수 있다.

이 애플리케이션의 Cookie값, 즉 JSESSIONID 를 지우고 다른 페이지를 요청하면 login이 풀린다.

그래도 다시 서버에서 JSESSIONID를 받게 되니 이 다음번 요청부터는 로그인을 하면 계속 로그인 상태가 유지된다.



📝 로그아웃 기능 만들기

1. header.jsp 파일에 로그아웃 만들기

  <c:choose>
    <c:when test="${empty principal}">
      <li class="nav-item"><a class="nav-link" href="/loginForm">로그인</a></li>
      <li class="nav-item"><a class="nav-link" href="/joinForm">회원가입</a></li>
    </c:when>
    <c:otherwise>
      <li class="nav-item"><a class="nav-link" href="/boards/writeForm">글쓰기</a></li>
      <li class="nav-item"><a class="nav-link" href="/logout">로그아웃</a></li>
    </c:otherwise>
  </c:choose>

2. Controller

코드로 로그아웃한다는 말은 = 세션에서 해당 사용자의 키값의 데이터만 삭제한다는 뜻이다. (세션 전체X)
이 역할을 하는 메서드가 invalidate 이다.

	@GetMapping("/logout")
	public String logout() {
		session.invalidate();
		return "redirect:/";
	}
profile
코딩을 배우고 기록합니다. 읽는 사람이 이해하기 쉽게 쓰려고 합니다.

0개의 댓글