[Spring Boot][5] 6. 로그인 처리1 - 쿠키, 세션

sorzzzzy·2021년 9월 23일
6

Spring Boot - RoadMap 1

목록 보기
43/46
post-thumbnail

🏷 로그인 요구사항


✔️ 요구사항

  • 홈 화면 - 로그인 전

    • 회원 가입
    • 로그인
  • 홈 화면 - 로그인 후

    • 본인 이름('누구'님 환영합니다.)
    • 상품 관리
    • 로그 아웃
  • 보안 요구사항

    • 로그인 사용자만 상품에 접근하고, 관리할 수 있음
    • 로그인 하지 않은 사용자가 상품 관리에 접근하면 로그인 화면으로 이동
  • 회원 가입, 상품 관리



🏷 프로젝트 생성

이전 프로젝트에 이어서 로그인 기능을 학습해보자❗️


✔️ 프로젝트 설정

1️⃣ login-start의 폴더 이름을 login으로 변경
2️⃣ ItemServiceApplication.main() 을 실행해서 프로젝트가 정상 수행되는지 확인


✔️ 패키지 구조

  • hello.login
  • domain
    • item
    • member
    • login
  • web
    • item
    • member
    • login

💡 도메인이 가장 중요하다 💡

  • 향후 web을 다른 기술로 바꾸어도 도메인은 그대로 유지할 수 있어야 한다.
  • 이렇게 하려면 web은 domain을 알고있지만 domain은 web을 모르도록 설계해야 한다.
  • 이것을 web은 domain을 의존하지만, domain은 web을 의존하지 않는다고 표현한다.


🏷 홈 화면

✔️ HomeController - home() 수정

@GetMapping("/")
public String home() {
    return "home";
}

✔️ templates/home.html 추가

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link th:href="@{/css/bootstrap.min.css}"
          href="../css/bootstrap.min.css" rel="stylesheet">
</head>
<body>

<div class="container" style="max-width: 600px">
    <div class="py-5 text-center">
        <h2>홈 화면</h2>
    </div>

    <div class="row">
        <div class="col">
            <button class="w-100 btn btn-secondary btn-lg" type="button"
                    th:onclick="|location.href='@{/members/add}'|">
                회원 가입
            </button>
        </div>
        <div class="col">
            <button class="w-100 btn btn-dark btn-lg" onclick="location.href='items.html'"
                    th:onclick="|location.href='@{/login}'|" type="button">
                로그인
            </button>
        </div>
    </div>

    <hr class="my-4">

</div> <!-- /container -->

</body>
</html>



🏷 회원 가입

✔️ Member

package hello.login.domain.member;

import lombok.Data;
import javax.validation.constraints.NotEmpty;

@Data
public class Member {

    private Long id;

    @NotEmpty
    // 로그인 ID
    private String loginId;
    @NotEmpty
    // 사용자 이름
    private String name;
    @NotEmpty
    private String password;
}

✔️ MemberRepository

package hello.login.domain.member;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;

import java.util.*;

/**
 * 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
 */
@Slf4j
@Repository
public class MemberRepository {

    private static Map<Long, Member> store = new HashMap<>();   // static 사용
    private static long sequence = 0L;  // static 사용

    public Member save(Member member) {
        member.setId(++sequence);
        log.info("save: member={}", member);
        store.put(member.getId(), member);
        return member;
    }

    public Member findById(Long id) {
        return store.get(id);
    }

    public Optional<Member> findByLoginId(String loginId) {
        return findAll().stream()
                .filter(m -> m.getLoginId().equals(loginId))
                .findFirst();
    }

    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }

    public void clearStore() {
        store.clear();
    }
}

✔️ MemberController

package hello.login.web.member;

import hello.login.domain.member.Member;
import hello.login.domain.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.validation.Valid;

@Controller
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {

    private final MemberRepository memberRepository;

    @GetMapping("/add")
    public String addForm(@ModelAttribute("member") Member member) {
        return "members/addMemberForm";
    }

    @PostMapping("/add")
    public String save(@Valid @ModelAttribute Member member, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "members/addMemberForm";
        }

        memberRepository.save(member);
        return "redirect:/";
    }
}

📌 @ModelAttribute("member")@ModelAttribute 로 변경해도 결과는 같다.
여기서는 IDE에서 인식 문제가 있어서 적용했다.


✔️ 회원 가입 뷰 템플릿

templates/members/addMemberForm.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link th:href="@{/css/bootstrap.min.css}"
          href="../css/bootstrap.min.css" rel="stylesheet">
    <style>
        .container {
            max-width: 560px;
        }
        .field-error {
            border-color: #dc3545;
            color: #dc3545;
        }
    </style>
</head>
<body>

<div class="container">

    <div class="py-5 text-center">
        <h2>회원 가입</h2>
    </div>

    <h4 class="mb-3">회원 정보 입력</h4>

    <form action="" th:action th:object="${member}" method="post">

        <div th:if="${#fields.hasGlobalErrors()}">
            <p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="${err}">전체 오류 메시지</p>
        </div>

        <div>
            <label for="loginId">로그인 ID</label>
            <input type="text" id="loginId" th:field="*{loginId}" class="form-control"
                   th:errorclass="field-error">
            <div class="field-error" th:errors="*{loginId}" />
        </div>
        <div>
            <label for="password">비밀번호</label>
            <input type="password" id="password" th:field="*{password}" class="form-control"
                   th:errorclass="field-error">
            <div class="field-error" th:errors="*{password}" />
        </div>
        <div>
            <label for="name">이름</label>
            <input type="text" id="name" th:field="*{name}" class="form-control"
                   th:errorclass="field-error">
            <div class="field-error" th:errors="*{name}" />
        </div>


        <hr class="my-4">

        <div class="row">
            <div class="col">
                <button class="w-100 btn btn-primary btn-lg" type="submit">회원 가입</button>
            </div>
            <div class="col">
                <button class="w-100 btn btn-secondary btn-lg" onclick="location.href='items.html'"
                        th:onclick="|location.href='@{/}'|"
                        type="button">취소</button>
            </div>
        </div>

    </form>

</div> <!-- /container -->
</body>
</html>

✔️ TestDataInit

회원용 테스트 데이터 추가

  • loginId : test
  • password : test!
  • name : 테스터
package hello.login;

import hello.login.domain.item.Item;
import hello.login.domain.item.ItemRepository;
import hello.login.domain.member.Member;
import hello.login.domain.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Component
@RequiredArgsConstructor
public class TestDataInit {

    private final ItemRepository itemRepository;
    private final MemberRepository memberRepository;

    /**
     * 테스트용 데이터 추가
     */
    @PostConstruct
    public void init() {
        itemRepository.save(new Item("itemA", 10000, 10));
        itemRepository.save(new Item("itemB", 20000, 20));

        Member member = new Member();
        member.setLoginId("test");
        member.setPassword("test!");
        member.setName("테스터");

        memberRepository.save(member);
    }
}


🏷 로그인 기능

로그인 ID, 비밀번호를 입력하는 부분에 집중하여 로그인 기능을 개발해보자😀


✔️ LoginService

package hello.login.domain.login;

import hello.login.domain.member.Member;
import hello.login.domain.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LoginService {

    private final MemberRepository memberRepository;

    /**
     * @return null 로그인 실패
     */
    public Member login(String loginId, String password) {
        return memberRepository.findByLoginId(loginId)
                .filter(m -> m.getPassword().equals(password))
                .orElse(null);
    }
}

⬆️ 로그인의 핵심 비즈니스 로직은 회원을 조회한 다음에 파라미터로 넘어온 password와 비교해서, 같으면 회원을 반환하고, 다르면 null을 반환한다!


✔️ LoginForm

package hello.login.web.login;

import lombok.Data;

import javax.validation.constraints.NotEmpty;

@Data
public class LoginForm {

    @NotEmpty
    private String loginId;

    @NotEmpty
    private String password;
}

✔️ LoginController

package hello.login.web.login;
import hello.login.domain.login.LoginService;
import hello.login.domain.member.Member;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.util.Objects;

@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {

    private final LoginService loginService;
    
    @GetMapping("/login")
    public String loginForm(@ModelAttribute("loginForm") LoginForm form) {
        return "login/loginForm";
    }
    
    @PostMapping("/login")
    public String login(@Valid @ModelAttribute LoginForm form, BindingResult
bindingResult) {
        if (bindingResult.hasErrors()) {
            return "login/loginForm";
	}
        
        Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
        
        log.info("login? {}", loginMember);
          if (loginMember == null) {
            bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
            return "login/loginForm";
          }
        return "redirect:/";
        }
}
  • 로그인 서비스를 호출해서 로그인에 성공하면 홈 화면으로 이동하고, 로그인에 실패하면 bindingResult.reject() 를 사용해서 글로벌 오류(ObjectError)를 생성
  • 그리고 정보를 다시 입력하도록 로그인 폼을 뷰 템플릿으로 사용

✔️ 실행

  • 로그인이 성공하면 홈으로 이동하고, 로그인에 실패하면 "아이디 또는 비밀번호가 맞지 않습니다."라는 경고와 함께 로그인 폼이 나타난다👍🏻
  • 그러나 아직 로그인 성공 시, 홈 화면에 고객 이름이 보이진 않는데 이름을 보여주려면 어떻게 해야할까🤔❓


🏷 로그인 처리하기 - 쿠키 사용

위의 문제를, 쿠키를 사용해서 로그인, 로그아웃 기능을 구현해 해결해보자😊

📌 쿠키에는 영속 쿠키와 세션 쿠키가 있다.

  • 영속 쿠키 : 만료 날짜를 입력하면 해당 날짜까지 유지
  • 세션 쿠키 : 만료 날짜를 생략하면 브라우저 종료시 까지만 유지
    ➡️ 브라우저 종료시 로그아웃이 되어야 하므로, 우리에게 필요한 것은 세션 쿠키이다!

✔️ LoginController - login()

로그인에 성공했을 때 세션 쿠키를 생성하자!

public String login(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse response) {
        if (bindingResult.hasErrors()) {
            return "login/loginForm";
        }

        Member loginMember = loginService.login(form.getLoginId(), form.getPassword());

        if (loginMember == null) {
            bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
            return "login/loginForm";
        }

        //로그인 성공 처리

        //쿠키에 시간 정보를 주지 않으면 세션 쿠기(브라우저 종료시 모두 종료)
        Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
        response.addCookie(idCookie);
        return "redirect:/";

    }

📌 쿠키 생성 로직

Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
response.addCookie(idCookie);
  • 로그인에 성공하면 쿠키를 생성하고 HttpServletResponse에 담는다.
  • 쿠키 이름은 memberId 이고, 값은 회원의 id를 담아둔다.
  • 웹 브라우저는 종료 전까지 회원의 id를 서버에 계속 보내줄 것이다.

✔️ 홈 - 로그인 처리

요구사항에 맞추어 로그인에 성공하면 로그인 한 사용자 전용 홈 화면을 보여줘야 한다!

코드를 입력하세요

➡️ @CookieValue 를 사용하면 편리하게 쿠키를 조회할 수 있다.
➡️ 로그인 하지 않은 사용자도 홈에 접근할 수 있기 때문에 required = false 를 사용한다.

  • 로그인 쿠키(memberId)가 없는 사용자는 기존 home으로 보낸다. 만약 로그인 쿠키가 있어도 회원이 없다면 home으로 보낸다
  • 로그인 쿠키(memberId)가 있는 사용자는 로그인 사용자 전용 홈 화면인 loginHome으로 보낸다.
  • 홈 화면에 화원 관련 정보도 출력해야 해서 member 데이터도 모델에 담아서 전달한다.

✔️ LoginController - logout 기능 추가

public String logout(HttpServletResponse response) {
        expireCookie(response, "memberId");
        return "redirect:/";
    }

🏷 쿠키와 보안 문제

쿠키를 사용하면 로그인을 유지할 순 있지만 심각한 보안문제가 뒤따른다.

바로!!!!!!!!!!!!!!

1️⃣ 쿠키 값은 임의로 변경할 수 있다.
2️⃣ 쿠키에 보관된 정보는 훔쳐갈 수 있다.
3️⃣ 해커가 쿠키를 한번 훔쳐가면 평생 사용할 수 있다.

이를 위해 우리는 앞으로 세션을 통해 문제를 해결해 보려고 한다😊



🏷 로그인 처리하기 - 세션 동작 방식

세션을 어떻게 개발할지 먼저 개념을 이해해보자.

⬆️ 사용자가 loginId , password 정보를 전달하면 서버에서 해당 사용자가 맞는지 확인한다.

⬆️ 세션 ID를 생성하는데, 추정 불가능해야 한다.

⬆️ 서버는 클라이언트에 mySessionId 라는 이름으로 세션ID 만 쿠키에 담아서 전달하고, 클라이언트는 쿠키 저장소에 mySessionId 쿠키를 보관한다.

💡 중요

  • 회원과 관련된 정보는 전혀 클라이언트에 전달하지 않는다는 것이다.
  • 오직 추정 불가능한 세션 ID만 쿠키를 통해 클라이언트에 전달한다.

⬆️ 클라이언트는 요청시 항상 mySessionId 쿠키를 전달하고, 서버에서는 클라이언트가 전달한 mySessionId 쿠키 정보로 세션 저장소를 조회해서 로그인시 보관한 세션 정보를 사용한다.



🏷 로그인 처리하기 - 세션 직접 만들기

세션 관리는 다음 3가지 기능을 제공한다😊
1️⃣ 세션 생성
➡️ sessionId 생성 (임의의 추정 불가능한 랜덤 값)
➡️ 세션 저장소에 sessionId와 보관할 값 저장
➡️ sessionId로 응답 쿠키를 생성해서 클라이언트에 전달
2️⃣ 세션 조회
➡️ 클라이언트가 요청한 sessionId 쿠키의 값으로, 세션 저장소에 보관한 값 조회
3️⃣ 세션 만료
➡️ 클라이언트가 요청한 sessionId 쿠키의 값으로, 세션 저장소에 보관한 sessionId와 값 제거


✔️ SessionManager - 세션 관리

package hello.login.web.session;

import org.springframework.stereotype.Component;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 세션 관리
 */
@Component
public class SessionManager {

    // 쿠키를 쓸 곳이 많기 때문에 상수로 만듦
    public static final String SESSION_COOKIE_NAME = "mySessionId";

    // 스프링 아이디와 객체를 맵으로 저장
    // 동시성을 위해 ConcurrentHashMap<>() 사용
    private Map<String, Object> sessionStore = new ConcurrentHashMap<>();

    /**
     * 세션 생성
     */
    public void createSession(Object value, HttpServletResponse response) {

        // 세션 id를 생성하고, 값을 세션에 저장
        // randomUUID() : 확실한 랜덤값을 얻을 수 있음. 자바가 제공
        String sessionId = UUID.randomUUID().toString();
        sessionStore.put(sessionId, value);

        // 쿠키 생성
        Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
        response.addCookie(mySessionCookie);
    }

    /**
     * 세션 조회
     */
    public Object getSession(HttpServletRequest request) {
        // 쿠기를 찾음
        Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
        if (sessionCookie == null) {
            return null;
        }
        return sessionStore.get(sessionCookie.getValue());
    }

    /**
     * 세션 만료
     */
    public void expire(HttpServletRequest request) {
        Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
        if (sessionCookie != null) {
            // 만료된 쿠키를 지움
            sessionStore.remove(sessionCookie.getValue());
        }
    }

    // 쿠키를 찾는 로직
    public Cookie findCookie(HttpServletRequest request, String cookieName) {
        if (request.getCookies() == null) {
            return null;
        }
        return Arrays.stream(request.getCookies())
                .filter(cookie -> cookie.getName().equals(cookieName))
                .findAny()
                .orElse(null);
    }

}


🏷 로그인 처리하기 - 직접 만든 세션 적용

✔️ LoginController - loginV2()

1️⃣ 로그인

  • private final SessionManager sessionManager; 주입
  • sessionManager.createSession(loginMember, response);
    ➡️ 로그인 성공시 세션을 등록한다. 세션에 loginMember 를 저장해두고, 쿠키도 함께 발행한다.

2️⃣ 로그아웃

  • sessionManager.expire(request);
  • 로그 아웃시 해당 세션의 정보를 제거한다.

✔️ HomeController - homeLoginV2()

  • private final SessionManager sessionManager; 주입
  • 세션 관리자에서 저장된 회원 정보를 조회한다.
    만약 회원 정보가 없으면, 쿠키나 세션이 없는 것 이므로 로그인 되지 않은 것으로 처리한다.


🏷 로그인 처리하기 - 서블릿 HTTP 세션1

바로 이전과 같이, 프로젝트마다 이러한 세션 개념을 직접 개발하는 것은 상당히 불편하다☹️

이제 직접 만드는 세션 말고, 서블릿이 공식 지원하는 세션을 알아보자!!
서블릿이 공식 지원하는 세션은 우리가 직접 만든 세션과 동작 방식이 거의 같고, 추가로 세션을 일정시간 사용하지 않으면 해당 세션을 삭제하는 기능을 제공한다👍🏻

📌 HttpSession 소개

  • 서블릿이 제공하는 HttpSession 도 결국 우리가 직접 만든 SessionManager 와 같은 방식으로 동작한다.
  • 서블릿을 통해 HttpSession 을 생성하면 다음과 같은 쿠키를 생성한다.
  • 쿠키 이름이 JSESSIONID 이고, 값은 추정 불가능한 랜덤 값이다.

✔️ LoginController - loginV3()

1️⃣ 로그인

  • 세션을 생성하려면 request.getSession(true) 를 사용한다.
    ➡️ public HttpSession getSession(boolean create);
  • request.getSession(true)
    • 세션이 있으면 기존 세션을 반환한다.
    • 세션이 없으면 새로운 세션을 생성해서 반환한다.
  • request.getSession(false)
    • 세션이 있으면 기존 세션을 반환한다.
    • 세션이 없으면 새로운 세션을 생성하지 않고 null 을 반환한다.
  • request.getSession()
    • 신규 세션을 생성하는 request.getSession(true)와 동일하다

2️⃣ 로그아웃

  • session.invalidate()
    ➡️ 세션을 제거한다.

✔️ HomeController - homeLoginV3()

  • request.getSession(false)

    • request.getSession() 를 사용하면 기본 값이 create: true 이므로, 로그인 하지 않을 사용자도 의미없는 세션이 만들어진다.
    • 따라서 세션을 찾아서 사용하는 시점에는 create: false 옵션을 사용해서 세션을 생성하지 않아야 한다.
  • session.getAttribute(SessionConst.LOGIN_MEMBER)

    • 로그인 시점에 세션에 보관한 회원 객체를 찾는다.


🏷 로그인 처리하기 - 서블릿 HTTP 세션2

📌 @SessionAttribute
스프링은 세션을 더 편리하게 사용할 수 있도록 @SessionAttribute 을 지원한다.

이미 로그인 된 사용자를 찾을 때는 다음과 같이 사용하면 된다. (참고로 이 기능은 세션을 생성하지 않는다!)
➡️ @SessionAttribute(name = "loginMember", required = false) Member loginMember


✔️ HomeController - homeLoginV3Spring()

  • @SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model)
    • 세션을 찾고, 세션에 들어있는 데이터를 찾는 번거로운 과정을 스프링이 한번에 편리하게 처리해준다.

📌 TrackingModes
URL 전달 방식을 끄고 항상 쿠키를 통해서만 세션을 유지하고 싶으면 다음과 같이 속성 변경을 해주자!
application.properties
➡️ server.servlet.session.tracking-modes=cookie



🏷 세션 타임아웃 설정

세션에 타임아웃을 설정해보자😊


1️⃣ 스프링 부트로 글로벌 설정

➡️ application.properties server.servlet.session.timeout=60

  • 60초, 기본은 1800(30분)
  • 글로벌 설정은 분 단위로 설정해야 한다. 60(1분), 120(2분), ...

2️⃣ 특정 세션 단위로 시간 설정

➡️ session.setMaxInactiveInterval(1800);

  • 1800초


🏷 최종 코드

✔️ HomeController

@Slf4j
@Controller
@RequiredArgsConstructor
public class HomeController {

    private final MemberRepository memberRepository;
    private final SessionManager sessionManager;

//    @GetMapping("/")
    public String home() {
        return "home";
    }

//    @GetMapping("/")
    public String homeLogin(@CookieValue(name = "memberId", required = false) Long memberId, Model model) {

        if (memberId == null) {
            return "home";
        }

        //로그인
        Member loginMember = memberRepository.findById(memberId);
        if (loginMember == null) {
            return "home";
        }

        model.addAttribute("member", loginMember);
        return "loginHome";
    }

    // 직접 만든 세션 사용
//    @GetMapping("/")
    public String homeLoginV2(HttpServletRequest request, Model model) {

        // 세션 관리자에 저장된 회원 정보 조회
        // Member 로 캐스팅
        Member member = (Member)sessionManager.getSession(request);

        // 로그인
        if (member == null) {
            return "home";
        }

        model.addAttribute("member", member);
        return "loginHome";
    }

    // 서블릿 HTTP 세션 사용
//    @GetMapping("/")
    public String homeLoginV3(HttpServletRequest request, Model model) {
        // getSession(true) 를 사용하면 처음 들어온 사용자도 세션이 만들어지기 때문에 false로 받음
        HttpSession session = request.getSession(false);
        if (session == null) {
            return "home";
        }

        // Member 로 타입 캐스팅
        Member loginMember = (Member)session.getAttribute(SessionConst.LOGIN_MEMBER);

        // 세션에 회원 데이터가 없으면 home
        if (loginMember == null) {
            return "home";
        }

        // 세션이 유지되면 로그인으로 이동
        model.addAttribute("member", loginMember);
        return "loginHome";
    }

    // @SessionAttribute 사용
    @GetMapping("/")
    public String homeLoginV3Spring(
            // @SessionAttribute 사용
            @SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model) {

        // 세션에 회원 데이터가 없으면 home
        if (loginMember == null) {
            return "home";
        }

        // 세션이 유지되면 로그인으로 이동
        model.addAttribute("member", loginMember);
        return "loginHome";
    }

//    @GetMapping("/")
    public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) {

        //세션에 회원 데이터가 없으면 home
        if (loginMember == null) {
            return "home";
        }

        //세션이 유지되면 로그인으로 이동
        model.addAttribute("member", loginMember);
        return "loginHome";
    }
}

✔️ LoginController

@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {

    private final LoginService loginService;
    // 세션
    private final SessionManager sessionManager;

    @GetMapping("/login")
    public String loginForm(@ModelAttribute("loginForm") LoginForm form) {
        return "login/loginForm";
    }

//    @PostMapping("/login")
    public String login(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse response) {
        if (bindingResult.hasErrors()) {
            return "login/loginForm";
        }

        Member loginMember = loginService.login(form.getLoginId(), form.getPassword());

        if (loginMember == null) {
            bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
            return "login/loginForm";
        }

        //로그인 성공 처리

        //쿠키에 시간 정보를 주지 않으면 세션 쿠기(브라우저 종료시 모두 종료)
        Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
        response.addCookie(idCookie);
        return "redirect:/";

    }

    // 직접 만든 세션 사용
//    @PostMapping("/login")
    public String loginV2(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse response) {
        if (bindingResult.hasErrors()) {
            return "login/loginForm";
        }

        Member loginMember = loginService.login(form.getLoginId(), form.getPassword());

        if (loginMember == null) {
            bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
            return "login/loginForm";
        }

        //로그인 성공 처리

        //세션 관리자를 통해 세션을 생성하고, 회원 데이터 보관
        sessionManager.createSession(loginMember, response);

        return "redirect:/";

    }

    // 서블릿 HTTP 세션 사용
//    @PostMapping("/login")
    public String loginV3(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletRequest request) {
        if (bindingResult.hasErrors()) {
            return "login/loginForm";
        }

        Member loginMember = loginService.login(form.getLoginId(), form.getPassword());

        if (loginMember == null) {
            bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
            return "login/loginForm";
        }

        // 로그인 성공 처리
        // 세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성해서 반환
        // getSession() : 디폴트가 true, false는 세션이 없을 때 새로 만들지 않고 null을 반환
        HttpSession session = request.getSession();
        // 세션에 로그인 회원 정보 보관
        session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

        return "redirect:/";
    }


    @PostMapping("/login")
    public String loginV4(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult,
                          @RequestParam(defaultValue = "/") String redirectURL,
                          HttpServletRequest request) {

        if (bindingResult.hasErrors()) {
            return "login/loginForm";
        }

        Member loginMember = loginService.login(form.getLoginId(), form.getPassword());

        if (loginMember == null) {
            bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
            return "login/loginForm";
        }

        //로그인 성공 처리
        //세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
        HttpSession session = request.getSession();
        //세션에 로그인 회원 정보 보관
        session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

        return "redirect:" + redirectURL;

    }

//    @PostMapping("/logout")
    public String logout(HttpServletResponse response) {
        expireCookie(response, "memberId");
        return "redirect:/";
    }

    // 직접 만든 세션 사용
//    @PostMapping("/logout")
    public String logoutV2(HttpServletRequest request) {
        // 세션 만료 적용
        sessionManager.expire(request);
        return "redirect:/";
    }

    // 서블릿 HTTP 세션 사용
    @PostMapping("/logout")
    public String logoutV3(HttpServletRequest request) {
        // getSession(false) 를 사용해야 함 (세션이 없더라도 새로 생성하면 안되기 때문)
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.invalidate();
        }
        return "redirect:/";
    }

    private void expireCookie(HttpServletResponse response, String cookieName) {
        Cookie cookie = new Cookie(cookieName, null);
        cookie.setMaxAge(0);
        response.addCookie(cookie);
    }
}

배고파요.. 밥 먹으러 갈래요....

profile
Backend Developer

0개의 댓글