카테고리 없음

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

daram 2022. 3. 25. 16:27

로그인 기능 구현은 일반적으로 세션을 이용한다고 합니다.

그럼에도 불구하고 쿠키를 사용하여 로그인 기능을 구현해 보는 이유는, 쿠키 동작 원리에 대해 자세히 알아보기 위함입니다.

 

만들어볼 내용은 간단합니다.

 

사용자가 홈 화면에 접근 하는 경우 (HomeController.java)

사용자 정보가 일치하는 쿠키를 가지고 있는 경우 -> 홈 화면으로 이동

사용자 정보와 일치하지 않는 쿠키를 가지고 있는 경우 -> 로그인 화면으로 이동

쿠키가 존재하지 않는 경우 -> 로그인 화면으로 이동

 

사용자가 로그인 화면에 접근 하는 경우 (LoginController.java)

사용자가 db에 존재하는 id와 패스워드를 입력하는 경우 -> 홈 화면으로 이동

사용자가 db에 존재하지 않는 id와 패스워드를 입력하는 경우 -> 에러 메세지를 로그인 화면에 출력

 

사용자가 회원가입 화면에 접근 하는 경우 (MemberController.java)

회원가입 진행

 

 

도메인 생성


Member.java

package hello.login.domain.member;

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

@Data
public class Member {

    private Long id;

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

 

LoginForm.java

로그인을 할 때 사용하는 Member dto 클래스이다.

로그인을 할 때는 loginId와 password에 대한 필드만 있으면 되기 때문에 아래와 같은 dto를 이용하여 사용하도록 하자.

package hello.login.web.login;

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

@Data
public class LoginForm {

    @NotEmpty
    private String loginId;

    @NotEmpty
    private String password;
}

 

저장소 생성


 

MemberRepository

db에 저장하는 save 클래스와

db로 부터 데이터를 찾아오는 findById, findAll, findByLoginId 클래스가 존재한다.

package hello.login.domain.member;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;

@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 List<Member> findAll() {
        return new ArrayList<>(store.values());
    }

    // 값이 null을 직접 반환하는 대신 optional로 반환하는게 현재 스타일
    public Optional<Member> findByLoginId(String loginId) {
        return findAll().stream()
            .filter(member -> member.getLoginId().equals(loginId))
            .findFirst();
    }

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

서비스 생성


 

LoginService.java

로그인 서비스 클래스이다.

사용자가 id와 password를 이용했을 때, db에 일치하는 id와 password가 존재하면 Member 객체를 반환하고, 존재하지 않는다면 Null 값을 반환한다.

 

package hello.login.domain.login;

import hello.login.domain.member.Member;
import hello.login.domain.member.MemberRepository;
import java.util.Optional;
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(member -> member.getPassword().equals(password))
            .orElse(null);
    }
}

 

컨트롤러 생성


LoginController

로그인 화면에서의 동작이다.

package hello.login.web.login;

import hello.login.domain.login.LoginService;
import hello.login.domain.member.Member;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

@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, 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";
        }

        // 로그인 성공 처리 TODO

        // 쿠키 사용
        // 쿠키에 시간 정보를 주지 않으면 세션 쿠키가 만들어진다. (브라우저 종료시 사라지는 쿠키)
        // 쿠키이름은 memberId고 값은 회원의 id를 담는다.
        // 웹 브라우저는 종료전까지 회원의 id를 전달해준다.
        Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
        // http response에 쿠키를 넘어서 전달해주자
        response.addCookie(idCookie);

        return "redirect:/";
    }
}

 

아래와 같이 진행이 되고 있는 것이다.

 

LoginController 진행

다시 살펴봅시다.

쿠키는 아래와 같이 발행할 수 있습니다.

쿠키에 들어가는 value type이 String 이기 때문에 형 변환해서 넣어줍니다.

Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));

 

만든 쿠키는 response에 담아서 client에 전달해줍니다.

response.addCookie(idCookie);

 

브라우저에서 쿠키가 들어간 것을 확인할 수 있습니다.

여기까지가 로그인을 했을 때 쿠키를 만들어 클라이언트에게 전달하기 까지의 과정입니다.

 

 

이제 홈 화면을 왔을 때 기능을 구현해야합니다.

홈 화면을 왔을 때 쿠키가 있으면 메인화면으로 이동하고

홈 화면을 왔을 때 쿠키가 없으면 로그인 화면으로 이동하는 것입니다.

@RequiredArgsConstructor
@Controller
public class HomeController {

    private final MemberRepository memberRepository;

    @GetMapping("/")
    public String homeLogin(@CookieValue(name = "memberId", required = false) Long memberId,
        Model model) {
        Member member = memberRepository.findById(memberId);
        if (member == null) {
            return "home";
        }
        return "loginHome";
    }
}

 

CookiValue 라는 annotation으로 클라이언트에게 memberId가 key로 있는 쿠키를 들고 올 수 있습니다.

하지만 모두 사용자가 memberI를 쿠키로 들고 있지는 않기 때문에 required = false로 설정하여 쿠키가 필수가 아님을 알려줍니다.

@CookieValue(name = "memberId", required = false) Long memberId

 

logout을 구현해보자


 

HomeController.java

package hello.login.web;

import hello.login.domain.member.Member;
import hello.login.domain.member.MemberRepository;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Slf4j
@Controller
@RequiredArgsConstructor
public class HomeController {

    private final MemberRepository memberRepository;

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

    @GetMapping("/")
    public String loginHome(@CookieValue(value = "memberId", required = false) Long memberId,
        Model model) {
        // 로그인 쿠키가 없으면 홈으로 보내고
        // 쿠키가 있으면 db에서 찾고
        // db에 존재하지 않으면 홈으로 보내고
        // db에 존재하면 로그인한 화면으로 보낸다.
        if (memberId == null) {
            return "home";
        }

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

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

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

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

@CookieValue annotation을 이용하여 클라이언트로 부터 cookie 값을 받아야한다.

이때 cookie가 없는 사용자도 있을 수 있기 때문에 required=false로 지정하여 모든 사람이 접근 가능하도록 설정을 해 놓는다.

 

쿠키가 없으면 로그인 화면으로 이동

쿠키가 있으면 쿠키를 db 정보와 매칭 시켜 본 후 일치한다면 홈 화면으로 이동하게 만들어 놓았다.

 

 

MemberController.java

회원 가입을 진행하는 부분이다.

package hello.login.web.member;

import hello.login.domain.member.Member;
import hello.login.domain.member.MemberRepository;
import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
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;

@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")
    private String save(@Valid @ModelAttribute Member member, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "members/addMemberForm";
        }

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

 

 

쿠키의 라이프 사이클을 정리해보자.

 

쿠키 생성 (서버 -> 클라이언트)


서버에서 쿠키를 생성할 때 member id를 저장해주었다.

쿠키의 value type은 String 이라는 점을 알아두자.

서버에서 만든 쿠키는 response에 태워서 클라이언트로 전송해주었다.

Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
// http response에 쿠키를 넘어서 전달해주자
response.addCookie(idCookie);

 

클라이언트로 부터 쿠키를 받을 때  (클라이언트 -> 서버)


@CookieValue라는 annotation을 이용하여 받을 수 있었다.

Cookie에 값을 넣을 때는 String type이었지만 아래와 같이 annotation을 사용할 때는 자동 형 변환이 가능하다.

@CookieValue(value = "memberId", required = false) Long memberId

 

쿠키 삭제 (서버 -> 쿠키 , 로그아웃)


쿠키 삭제하는 방법은 age를 0으로 변경 후 쿠키를 다시 response에 담아서 보내면 된다.

cookie.setMaxAge(0);
response.addCookie(cookie);