로그인 기능 구현은 일반적으로 세션을 이용한다고 합니다.
그럼에도 불구하고 쿠키를 사용하여 로그인 기능을 구현해 보는 이유는, 쿠키 동작 원리에 대해 자세히 알아보기 위함입니다.
만들어볼 내용은 간단합니다.
사용자가 홈 화면에 접근 하는 경우 (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:/";
}
}
아래와 같이 진행이 되고 있는 것이다.
다시 살펴봅시다.
쿠키는 아래와 같이 발행할 수 있습니다.
쿠키에 들어가는 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);