기억하기 : select에서 login만 POST요청이다
기본적으로 select는 GET요청이다.
그런데 login은 POST(body)로 데이터를 받아 같은 데이터가 있는지 확인하는 select다. (찾아지면 login, null이면 login 실패)POST를 사용하는 이유 : login 정보는 너무 중요한 정보이므로 주소에 담기면 안 되기 때문
<select id="login" resultType="site.metacoding.red.domain.users.Users">
SELECT * FROM users WHERE username=#{username} AND password=#{password}
</select>
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"%>
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;
}
@PostMapping("/login")
public String login(LoginDto loginDto) {
return "redirect:/";
}
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);
}
@PostMapping("/login")
public String login(LoginDto loginDto) {
Users usersPS = usersDao.login(loginDto);
//DB에서 받은 데이터면 PS를 붙여주자.
//그래야 DB에서 받은건지, 사용자에게 받은건지 구분이 된다.
if(usersPS !=null){ // 인증됨
return"redirect:/";
}else { // 인증안됨
return "redirect:/loginForm";
}
로그인이 지속되려면 로그인이 되었다는 정보를 저장해야 하는데, request는 응답시 데이터가 날아가므로 저장하지 못한다. 👉 session이라는 server의 메모리 영역에 저장 (톰캣이 들고있음)
그런데 세션에 접근하려면 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
이라는 레퍼런스 주소(변수)는 세션 메모리에 접근하는 주소가 된다.
Dispatcher Sevlet이 제공해주는 HttpSession은 IOC 컨테이너에 하나의 오브젝트로 있다. 왜냐하면 IOC 컨테이너는 타입별로 하나씩만 띄울 수 있기 때문이다.
HttpSession은 JSESSIONID라는 이름의 세션을 자동으로 생성한다. 하나의 세션에는 여러 값을 저장할 수 있고, session.setAttribute()
로 데이터를 저장할 수 있다.
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";
}
}
}
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}
조건문을 사용하여 로그아웃 상태일 때 보일 header와 로그인 상태일 때 보일 header를 설정한다. Mybatis문법에서 if조건문은 else가 없으므로 choose-when otherwise 조건문을 사용해야 한다.
❓ ${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>
🌳→🌿 인증과 인가
select로 DB의 데이터를 찾았다고 해서 로그인이 된 것은 아니다. 이 사람이 username과 password가 정상이라는 인증이 되었을 뿐이다.
- 인증이 된 뒤 session에 session-id가 만들어져야 페이지의 기능을 이용할 수 있는 권한이 생긴다. 이를 인가라고 한다. session-id에는 User의 정보가 담긴 Object가 담겨 있다.
🤔 Session-id에 User object를 저장했기에 로그인이 되었다?
😄 저장할 때의 key값이 있으면 login이 되었다고 보기로 한 것 (header.jsp의 조건문으로 설정함)
Http는 Stateless의 특징을 가진다. 그런데 세션 영역에 세션 키값(principal)이 있어 다른 주소로 이동해도 로그인이 풀리지 않아 Stateful한 것처럼 보인다.
그러나 실제로 Stateful하다고 말하기 위해서는 서버가 소켓으로 계속 연결되어 있어야 한다.
Http는 다른 주소로 갈 때마다 Controller를 작동시키면서 데이터를 새로 불러오고(F5) 이 과정에서 브라우저의 세션 키값이 session에 있는 키값과 일치하는지 확인하는 session 인증이 일어날 뿐이다.
브라우저를 끄면 쿠키의 session key가 사라짐 ← session에는 여전히 값이 있음
⇒ 쿠키의 session key가 없기 때문에 세션에서 principal을 찾지 못한다.
일정 시간 이상 사용하지 않으면(보통 30분) sessionn에서 session key값을 삭제함 (web.xml에서 설정)
강제로 서버에서 session을 삭제하기
🌿→🌳 톰캣이 사용하는 메모리 영역
application : 서버가 꺼질 때까지 유지됨
session : 계속 유지되다 사용자가 브라우저를 끄거나 강제로 종료하면 사라짐
request : 요청하고 응답이 끝나면 사라짐
page
F12-Application-Cookies를 들어가면 제일 처음 서버에게 받은 JSESSIONID값이 있음을 알 수 있다.
이 애플리케이션의 Cookie값, 즉 JSESSIONID 를 지우고 다른 페이지를 요청하면 login이 풀린다.
그래도 다시 서버에서 JSESSIONID를 받게 되니 이 다음번 요청부터는 로그인을 하면 계속 로그인 상태가 유지된다.
<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>
코드로 로그아웃한다는 말은 = 세션에서 해당 사용자의 키값의 데이터만 삭제한다는 뜻이다. (세션 전체X)
이 역할을 하는 메서드가 invalidate
이다.
@GetMapping("/logout")
public String logout() {
session.invalidate();
return "redirect:/";
}