Cookie가 뭐야?
Cookie는 HTTP의 비연결성, 무상태성을 보완하기 위한 방법 중 하나로, 서버가 사용자의 웹 브라우저에 전송하는 작은 데이터 조각을 말한다. 서버가 보내준 조각(쿠키)를 브라우저가 저장을 해 놓았다가 동일한 서버에게 다시 요청을 보낼 때 조각(쿠키)와 함께 전송한다.
쿠키를 통해 동일한 사용자가 요청한 것인지 아닌지를 판단할 때 사용한다.
* HTTP의 비연결성
클라이언트가 서버에 요청했을 때, 그 요청에 맞는 응답을 보낸 후 연결을 끊는 처리 방식
* 무상태성
클라이언트와 첫 번째 통신에서 데이터를 주고받았다 해도, 두 번째 통신에서는 이전 데이터를 유지하지 않는다.
✅ HTTP의 비연결성, 무상태성을 보완하기 위해 Cookie 와 Session이 사용된다.
Cookie의 형태
쿠키는 <Key, Value> 형태로 구성된 String으로 4KB이상 저장할 수 없다.
보통 브라우저에 의해 저장되며, Request들의 Cookie HTTP 헤더 안에 포함되어 전송된다.
또한 브라우저마다 저장되는 쿠키가 다르다, 크롬에서 로그인해서 받은 쿠키는 파이어폭스에서 사용할 수 없다.
Cookie를 이용한 로그인 구현
- 로그인 성공 시 서버가 사용자에게 쿠키를 넘겨줌.
- 브라우저가 쿠키를 저장
- 사용자가 서버에 다음 요청을 할때마다 쿠기도 함께 서버에게 보냄.
- 서버는 사용자 요청에 담겨온 쿠키를 통해 로그인 했는지, 유저 정보, 권한 등을 확인할 수 있음.
쿠키 설정 방법
// 쿠키 설정
Cookie cookie = new Cookie([cookie-name], [cookie-value]);
// 쿠키 유효기간 설정
cookie.setMaxAge(60 * 60); // 60초 * 60번 == 1시간
// 쿠키를 응답 헤더에 추가
response.addCookie(cookie); // response는 HttpServletResponse 객체이다.
로그인 기능 목록
- 회원가입
- 아이디 중복 체크
- password == passwordCheck 체크
- nickName 중복 체크
- 로그인
- login password 체크
- 로그아웃
1. SpringBoot Initializr
dependency
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
2. Member Entity 생성
package com.millpre.cookielogin.member.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member {
@Id
@NotNull
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
private String loginId;
@NotNull
private String password;
@NotNull
private String nickName;
}
3. MemberRepository 생성
package com.millpre.cookielogin.member.entity.repository;
import com.millpre.cookielogin.member.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface MemberRepository extends JpaRepository<Member, Long> {
boolean existsByLoginId(String loginId); // 존재하는 loginId인지 확인
boolean existsByNickName(String nickName); // 존재하는 닉네임인지 확인
Optional<Member> findByLoginId(String loginId); // loginId로 Member찾기
}
4. JoinDto 생성
package com.millpre.cookielogin.member.entity.dto;
import com.millpre.cookielogin.member.entity.Member;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class JoinDto {
@NotBlank(message = "로그인 아이디가 비어있습니다.")
private String loginId;
@NotBlank(message = "비밀번호가 비어있습니다.")
private String password;
private String passwordCheck;
@NotBlank(message = "닉네임이 비어있습니다.")
private String nickName;
public Member toEntity() {
return Member.builder()
.loginId(this.loginId)
.nickName(this.nickName)
.password(this.password)
.build();
}
}
5. LoginDto 생성
package com.millpre.cookielogin.member.entity.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class LoginDto {
private String loginId;
private String password;
}
6. MemberService 생성
package com.cookie.login.service;
import com.cookie.login.domain.dto.JoinRequest;
import com.cookie.login.domain.dto.LoginRequest;
import com.cookie.login.domain.entity.User;
import com.cookie.login.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Service
@Transactional
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
/* loginId 중복 체크
* 중복O true
* 중복X false
* */
public boolean checkLoginIdDuplicate(String loginId) {
return userRepository.existsByLoginId(loginId);
}
/* nickName 중복 체크
* 중복O true
* 중복X false
* */
public boolean checkNickNameDuplicate(String nickName) {
return userRepository.existsByNickName(nickName);
}
/* 회원 가입 기능
* loginId, nickName 중복 체크는 Controller 에서 => 에러 메시지 출력을 위해서
* */
public void join(JoinRequest request) {
userRepository.save(request.toEntity());
}
/* 로그인 기능
* 로그인 아이디가 존재하지 않거나 비밀번호가 일치하지 않는 경우 null return */
public User login(LoginRequest request) {
Optional<User> optionalUser = userRepository.findByLoginId(request.getLoginId());
// loginId가 일치하는 User가 없으면 null return
if (optionalUser.isEmpty()) return null;
User user = optionalUser.get();
// 찾아온 User의 password와 입력된 password가 다르면 null return
if (!user.getPassword().equals(request.getPassword())) return null;
return user;
}
/* userId(Long)를 입력받아 User return 해주는 기능
* 인증, 인가 시 사용
* userId가 DB에 없거나, null이면 null return*/
public User getLoginUserById(Long userId) {
if (userId == null) return null;
Optional<User> optionalUser = userRepository.findById(userId);
if(optionalUser.isEmpty()) return null;
return optionalUser.get();
}
}
7. MemberController 생성
package com.cookie.login.controller;
import com.cookie.login.domain.UserRole;
import com.cookie.login.domain.dto.JoinRequest;
import com.cookie.login.domain.dto.LoginRequest;
import com.cookie.login.domain.entity.User;
import com.cookie.login.service.UserService;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.*;
@Controller
@RequiredArgsConstructor
@RequestMapping("/cookie-login")
public class HomeController {
private final UserService userService;
@GetMapping(value = {"/", ""})
public String home(@CookieValue(name="userId", required = false) Long userId,
Model model) {
model.addAttribute("loginType", "cookie-login");
model.addAttribute("pageName", "쿠키 로그인");
User loginUser = userService.getLoginUserById(userId);
System.out.println(loginUser);
if (loginUser != null) {
System.out.println(loginUser.getNickName());
model.addAttribute("nickname", loginUser.getNickName());
}
return "home";
}
@GetMapping("/join")
public String joinPage(Model model) {
model.addAttribute("loginType", "cookie-login");
model.addAttribute("pageName", "쿠키 로그인");
model.addAttribute("joinRequest", new JoinRequest());
return "join";
}
@GetMapping("/info")
public String userInfo(@CookieValue(name = "userId", required = false) Long userId, Model model) {
model.addAttribute("loginType", "cookie-login");
model.addAttribute("pageName", "쿠키 로그인");
User loginUser = userService.getLoginUserById(userId);
if(loginUser == null) {
return "redirect:/cookie-login/login";
}
model.addAttribute("loginId", loginUser.getLoginId());
model.addAttribute("nickname", loginUser.getNickName());
model.addAttribute("role", loginUser.getRole());
return "info";
}
@GetMapping("/admin")
public String adminPage(@CookieValue(name = "userId", required = false) Long userId, Model model) {
model.addAttribute("loginType", "cookie-login");
model.addAttribute("pageName", "쿠키 로그인");
User loginUser = userService.getLoginUserById(userId);
if (loginUser == null) {
return "redirect:/cookie-login/login";
}
if (!loginUser.getRole().equals(UserRole.ADMIN)) {
return "redirect:/cookie-login";
}
return "admin";
}
@GetMapping("/login")
public String login(Model model) {
model.addAttribute("loginType", "cookie-login");
model.addAttribute("pageName", "쿠키 로그인");
model.addAttribute("loginRequest", new LoginRequest());
return "login";
}
@PostMapping("/login")
public String login(@ModelAttribute LoginRequest loginRequest,
HttpServletResponse response,
BindingResult bindingResult,
Model model) {
model.addAttribute("loginType", "cookie-login");
model.addAttribute("pageName", "쿠키 로그인");
User user = userService.login(loginRequest);
if (user == null) {
bindingResult.reject("loginFail", "로그인 아이디 또는 비밀번호가 틀렸습니다.");
return "login";
}
// 로그인 성공
Cookie cookie = new Cookie("userId", String.valueOf(user.getId()));
cookie.setMaxAge(60 * 60); // 쿠키 유효 시간: 1시간
response.addCookie(cookie);
return "redirect:/cookie-login";
}
@PostMapping("/join")
public String join(@Valid @RequestBody JoinRequest joinRequest, BindingResult bindingResult, Model model) {
model.addAttribute("loginType", "cookie-login");
model.addAttribute("pageName", "쿠키 로그인");
// loginId 중복 확인
if (userService.checkLoginIdDuplicate(joinRequest.getLoginId())) {
bindingResult.addError(new FieldError("joinRequest", "loginId", "로그인 아이디가 중복됩니다."));
}
// 닉네임 중복 체크
if (userService.checkNickNameDuplicate(joinRequest.getNickName())) {
bindingResult.addError(new FieldError("joinRequest", "nickname", "닉네임이 중복됩니다."));
}
// password와 passwordCheck가 같은지 체크
if (!joinRequest.getPassword().equals(joinRequest.getPasswordCheck())) {
bindingResult.addError(new FieldError("joinRequest", "passwordCheck", "비밀번호가 일치하지 않습니다."));
}
if (bindingResult.hasErrors()) {
return "join";
}
userService.join(joinRequest);
return "redirect:/cookie-login";
}
@GetMapping("/logout")
public String logout(HttpServletResponse response, Model model) {
model.addAttribute("loginType", "cookie-login");
model.addAttribute("pageName", "쿠키 로그인");
Cookie cookie = new Cookie("userId", null);
cookie.setMaxAge(0);
response.addCookie(cookie);
return "redirect:/cookie-login";
}
}