Cookie
📌 사용자의 웹 브라우저에 저장되는 정보로 사용자의 상태 혹은 세션을 유지하거나 사용자 경험을 개선하기 위해 사용된다. 사용자 정보나 세션 데이터를 클라이언트(브라우저)에 저장하는 기술
- Cookie는 주로 사용자 세션 관리(로그인, 장바구니, 접속시간)나 광고 트래킹(사용자 행동) 등의 목적으로 사용된다.
- HTTP는 Stateless, Connectionless 특성을 가지고 있다.
- Client가 재요청시 Server는 이전 요청에 대한 정보를 기억하지 못한다.
- 로그인과 같이 상태를 유지해야 하는 경우가 발생한다.
- Request에 사용자 정보를 포함하면 해결이 된다.
- 로그인 후에는 사용자 정보와 관련된 값이 저장되어 있어야한다.
- 브라우저를 완전히 종료한 뒤 다시 열어도 사용자 정보가 유지되어야 한다.
서버에 전송하지 않고 브라우저에 단순히 데이터를 저장하고 싶다면 Web Storage(localStorage, sessionStorage)를 사용하면 된다. 하지만 보안에 취약하기 때문에 주민번호와 같은 민감정보를 저장하면 안된다. |
Web Storage는 클라이언트의 저장소로, 쿠키와 비교했을 때 서버로 데이터를 자동전송하지 않으며, 클라이언트에서만 데이터를 유지한다는 특징을 가지고 있다.
쿠키 (Cookie) | Web Storage (localStorage/sessionStorage) | |
저장 위치 | 클라이언트(브라우저), HTTP 요청 시 서버로 전송 | 클라이언트(브라우저), 서버로 자동 전송되지 않음 |
데이터 전송 | HTTP 요청 시 서버로 자동 전송 | 서버로 전송되지 않음 |
저장 용량 | 약 4KB | 약 5MB |
유효 기간 | 설정된 만료 시간까지 유지 | localStorage는 영구, sessionStorage는 세션 종료 시 삭제 |
범위 | 도메인/경로 단위로 설정 가능 | localStorage는 모든 탭에서 공유, sessionStorage는 현재 탭에 한정 |
보안 | HttpOnly와 Secure로 보안 강화 가능 | 자바스크립트를 통해 접근 가능 (XSS 공격에 취약) |
주 용도 | 사용자 상태 유지 (예: 로그인, 인증) | 사용자 설정, UI 상태, 임시 데이터 저장 |
데이터 접근 방식 | HTTP 헤더 또는 자바스크립트 API로 접근 | 자바스크립트 API로 접근 |
지원 브라우저 | 모든 브라우저에서 지원 | 최신 브라우저에서 지원 |
삭제 방식 | 쿠키 만료 시간 또는 브라우저 설정에서 삭제 | localStorage.clear(), sessionStorage.clear() |
- Cookie 찾아보기
- 브라우저 개발자도구(F12) → Application → Cookies
로그인 성공시 응답
Set-Cookie
- 로그인시 전달된 ID, Password로 User 테이블 조회하여 일치여부 확인
- 일치한다면 Set-Cookie를 활용해 Cookie에 사용할 값 저장
- Cookie는 보안에 취약하다.
로그인 이후 요청
요청 헤더 Cookie : 사용자 정보
- 로그인 이후에는 모든 요청마다 Request Header에 항상 Cookie 값을 담아서 요청한다.
- 클라이언트 → 서버 방향의 요청
- 네트워크 트래픽이 추가적으로 발생된다.
- 최소한의 정보만 사용해야한다.
- Cookie에 담겨있는 값으로 인증/인가 를 진행한다.
Cookie Header
📌 클라이언트(브라우저)가 서버에 HTTP 요청을 보낼 때, 클라이언트에 저장된 쿠키를 함께 전송하는 HTTP 요청 헤더입니다.
- 서버에서는 HTTP 응답 헤더에 Set-Cookie 속성을 사용해 생성하고 설정할 수 있다
- Cookie는 서버에서 생성되어 클라이언트에 전달, 저장된다.
Cookie Header
- Set-Cookie
- Server에서 Client로 Cookie 전달(Response Header)
- Cookie
- Client가 Cookie를 저장하고 HTTP 요청시 Server로 전달(Request Header)
Response 알아보기
set-cookie:
sessionId=abcd;
expires=Sat, 11-Dec-2024 00:00:00 GMT;
path=/;
domain=spartacodingclub.kr;
Secure
- Cookie의 생명주기
- 세션 Cookie
- 만료 날짜를 생략하면 브라우저 완전 종료시 까지만 유지된다.(Default)
- expires, max-age 가 생략된 경우
- 브라우저를 완전 종료 후 다시 페이지를 방문했을 때 다시 로그인을 해야한다.
- 만료 날짜를 생략하면 브라우저 완전 종료시 까지만 유지된다.(Default)
- 영속 Cookie
- 만료 날짜를 입력하면 해당 날짜까지 유지한다.
- expires=Sat, 11-Dec-2024 00:00:00 GMT;
- 해당 만료일이 도래하면 쿠키가 삭제된다.
- max-age=3600 (second, 3600초는 한시간. 60 * 60)
- 0이 되거나 음수를 지정하면 쿠키가 삭제된다.
- expires=Sat, 11-Dec-2024 00:00:00 GMT;
- 만료 날짜를 입력하면 해당 날짜까지 유지한다.
- 세션 Cookie
- Cookie의 도메인
- 쿠키가 아무 사이트에서나 생기고 동작하면 안된다!
- 필요없는 값 전송, 트래픽 문제 등이 발생한다.
- domain=spartacodingclub.kr
- domain=spartacodingclub.kr를 지정하여 쿠키를 저장한다.
- dev.spartacodingclub.kr와 같은 서브 도메인에서도 쿠키에 접근한다.
- domain을 생략하면 현재 문서 기준 도메인만 적용한다.
- 쿠키가 아무 사이트에서나 생기고 동작하면 안된다!
- Cookie의 경로
- 1차적으로 도메인으로 필터링 후 Path가 적용된다.
- 일반적으로 path=/ 루트(전체)로 지정한다.
- 위 경로를 포함한 하위 경로 페이지만 쿠키에 접근한다.
- path=/api 지정
- path=/api/example 가능
- path=/example 불가능
- path=/api 지정
- Cookie 보안
- Secure
- 기본적으로 Cookie는 http, https 구분하지 않고 전송한다.
- Secure를 적용하면 https인 경우에만 전송한다. s = Secure
- HttpOnly
- XSS(Cross-site Scripting) 공격을 방지한다.
- 악성 스크립트를 웹 페이지에 삽입하여 다른 사용자의 브라우저에서 실행되도록 하는 공격
- 자바스크립트에서 Cookie 접근을 못하도록 막는다.
- HTTP 요청시 사용한다.
- XSS(Cross-site Scripting) 공격을 방지한다.
- SameSite
- 비교적 최신 기능이라 브라우저 지원여부를 확인 해야한다.
- CSRF(Cross-Site Request Forgery) 공격을 방지한다.
- 사용자가 의도하지 않은 상태에서 특정 요청을 서버에 전송하게 하여 사용자 계정에서 원치 않는 행동을 하게 만든다.
- 요청 도메인과 쿠키에 설정된 도메인이 같은 경우만 쿠키 전송
- Secure
Cookie로 로그인 상태 유지하기
- 한번 로그인에 성공하면 HTTP Response에 쿠키를 담아서 브라우저에 전달한다.
- 브라우저는 요청마다 Cookie를 함께 전송한다.
- 보안상의 문제로 name=원욱이 아닌 userId=1과 같은 index 정보를 저장한다.
- 이것 또한 보안문제가 있다.
- 요구사항에 맞추어 세션 Cookie를 사용할지 영속 Cookie를 사용할지 결정한다.
- 코드예시
- 예시를 위해 ViewTemplate(Thymeleaf)을 사용하는 경우를 가정(SSR)
더보기
User 클래스
@Getter
public class User {
// 식별자
private Long id;
// 이름
private String name;
// 나이
private Integer age;
// 로그인 ID
private String userName;
// 비밀번호
private String password;
public User(Long id, String name, Integer age, String userName, String password) {
this.id = id;
this.name = name;
this.age = age;
this.userName = userName;
this.password = password;
}
}
- User 클래스 설계
로그인 요청 DTO
// 필드 전체를 매개변수로 가진 생성자가 있어야 @ModelAttribute가 동작한다.
@Getter
@AllArgsConstructor
public class LoginRequestDto {
// 사용자가 입력한 아이디
@NotBlank
private final String userName;
// 사용자가 입력한 비밀번호
@NotNull
private final String password;
}
- 일반적으로 DTO는 클라이언트의 요청혹은 서버의 응답이기 때문에 변경되면 안된다.
- final을 사용하여 불변 객체로 관리한다.
- Java17 버전에 나온 record를 사용할 수 있다.
로그인 응답 DTO
@Getter
public class LoginResponseDto {
private final Long id;
// 이외 응답에 필요한 데이터들을 필드로 구성하면 된다.
// 필요한 생성자
public LoginResponseDto(Long id) {
this.id = id;
}
}
유저 조회 응답 DTO
@Getter
public class UserResponseDto {
// 유저 식별자
private final Long id;
// 유저 이름
private final String name;
public UserResponseDto(Long id, String name) {
this.id = id;
this.name = name;
}
}
HomeController
@Controller
@RequiredArgsConstructor
public class HomeController {
private final UserService userService;
@GetMapping("/home")
public String home(
// @CookieValue(required = true) 로 필수값(default) 설정
// required = false 이면 필수값 아님.
@CookieValue(name = "userId", required = false) Long userId, // String->Long 자동 타입컨버팅
Model model
) {
// 쿠키에 값이 없으면 로그인 페이지로 이동 -> 로그인 X
if(userId == null) {
return "login";
}
// 실제 DB에 데이터 조회 후 결과가 없으면 로그인 페이지로 이동 -> 일치하는 회원정보 X
UserResponseDto loginUser = userService.findById(userId);
if(loginUser == null) {
return "login";
}
// 정상적으로 로그인 된 사람이라면 View에서 사용할 데이터를 model 객체에 데이터 임시 저장
model.addAttribute("loginUser", loginUser);
// home 화면으로 이동
return "home";
}
}
View에서는 model 객체에 담겨있는 loginUser 를 활용하여 변수로 사용할 수 있다.
UserController
@Controller
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping("/login")
public String login(
@Valid @ModelAttribute LoginRequestDto request,
HttpServletResponse response // 쿠키값 세팅에 필요
) {
// 로그인 유저 조회
LoginResponseDto responseDto = userService.login(request.getUserName(), request.getPassword());
if (responseDto.getId() == null) {
// 로그인 실패 예외처리
return "login";
}
// 로그인 성공 처리
// 쿠키 생성, Value는 문자열로 변환하여야 한다.
Cookie cookie = new Cookie("userId", String.valueOf(responseDto.getId()));
// 쿠키에 값 세팅 (expire 시간을 주지 않으면 세션쿠키가 됨, 브라우저 종료시 로그아웃)
// Response Set-Cookie: userId=1 형태로 전달된다.
response.addCookie(cookie);
// home 페이지로 리다이렉트
return "redirect:/home";
}
@PostMapping("/logout")
public String logout(
HttpServletResponse response
) {
Cookie cookie = new Cookie("userId", null);
// 0초로 쿠키를 세팅하여 사라지게 만듬
cookie.setMaxAge(0);
response.addCookie(cookie);
// home 페이지로 리다이렉트
return "redirect:/home";
}
}
- 로그인 기능
- 로그인에 성공하면 Cookie를 생성하고 HttpServletResponse 객체에 담는다.
- Cookie 이름(Key)은 userId , 값(Value)은 회원 index 값을 담아둔다.
- Set-Cookie: userId=1
- 만료 시간을 지정하지 않으면 세션 쿠키로 만들어진다.
- 브라우저 종료 전까지 userId 가 모든 요청 헤더의 Cookie에 담겨서 전달된다.
- 로그인에 성공하면 Cookie를 생성하고 HttpServletResponse 객체에 담는다.
- 로그아웃 기능
- 새로운 Cookie를 userId = null로 생성한다.
- setMaxAge(0) 설정으로 만료시킨다.
- 응답에 만료된 쿠키를 담아 보낸다.
UserService
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public LoginResponseDto login(String userName, String password) {
// 입력받은 userName, password와 일치하는 Database 조회
Long index = userRepository.findIdByUserNameAndPassword(userName, password);
return new LoginResponseDto(index);
}
public UserResponseDto findById(Long id) {
return userRepository.findById(id);
}
}
UserRepository
@Repository
public class UserRepository {
private static final User USER1 = new User(1L, "wonuk", 100, "wonuk", "1234");
private static final User USER2 = new User(2L, "wonuk2", 200, "wonuk2", "2345");
private static final List<User> USERS = Arrays.asList(USER1, USER2);
public Long findIdByUserNameAndPassword(String userName, String password) {
return USERS.stream()
.filter(user -> user.getUserName().equals(userName) && user.getPassword().equals(password))
.map(User::getId)
.findFirst()
.orElse(null);
}
public UserResponseDto findById(Long id) {
return USERS.stream()
.filter(user -> Objects.equals(user.getId(), id))
.map(user -> new UserResponseDto(user.getId(), user.getName()))
.findFirst()
.orElse(null);
}
}
실제 Database와 연동하지 않고, 상수로 미리 만든 User를 사용한다.
login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<h2>Login</h2>
<form th:action="@{/login}" method="post">
<div>
<label for="userName">Username:</label>
<input type="text" id="userName" name="userName" required>
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">Login</button>
</form>
</body>
</html>
home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Home</title>
</head>
<body>
<h1>Welcome Home!</h1>
<p th:text="'안녕하세요, ' + ${loginUser.name} + '님!'">Hello, User!</p>
<!-- 로그아웃 버튼 -->
<form th:action="@{/logout}" method="post" style="margin-top: 10px;">
<button type="submit">Logout</button>
</form>
</body>
</html>
브라우저 테스트
http://localhost:8080/home
- HomeController 호출
- Cookie에 값이 없는 상태로 login 페이지가 반환된다.
DB에 저장된 userName과 password로 로그인
- 로그인이 실패하면 login 페이지로 이동한다.
- 로그인이 성공하면 home 페이지로 리다이렉트
- 리 다이렉트되어 GET + /home 호출
- Cookie에 저장된 user 식별자 값으로 DB 조회
- 조회된 User를 Model에 추가
- home 페이지에서 Model을 참조하여 화면 출력
Cookie 문제점
📌 Cookie는 보안에 취약하여 userId=1 형태의 방식으로 로그인을 구현하지 않는다.
- = 쿠키에 민감한 정보를 직접 저장하거나 이를 기반으로 인증 로직을 구현하지 않는다
- userId=1은 데이터를 숨기지 않고 직접 대놓고 저장하는 방식
Cookie 문제점
쿠키 값은 임의로 변경할 수 있다.
- Client가 임의로 쿠키의 값을 변경하면 서버는 다른 유저로 인식한다.
- userId = 임의로 수정
- 브라우저 개발자도구(F12) → Application → Cookies → 값 수정 가능
- 실제로는 암호화되어 저장되어있는 Value들을 볼 수 있다!
Cookie에 저장된 Data는 탈취되기 쉽다.
- userId = 주민번호, userId = 인덱스 값
- 쿠키는 네트워크 전송 구간에서 탈취될 확률이 매우 높다.
- HTTPS를 사용하는 이유 중 하나에 속한다.
- 민감한 정보를 저장하면 안된다.
- 한번 탈취된 정보는 변경이 없다면 반영구적으로 사용할 수 있다.
보안 대처방법
- 쿠키에 중요한값을 저장하지 않는다.
- 사용자 별로 일반 유저나 해커들이 알아보지 못하는 값을 노출한다.
- 일반적으로 암호화된 Token을 쿠키에 저장한다.
- 서버에서 암호화된 Token과 사용자를 매핑해서 인식한다.
- Token은 서버에서 관리한다.
- 토큰은 해커가 임의의 값을 넣어도 동작하지 않도록 만들어야 한다.
- 해커가 토큰을 탈취해도 사용할 수 없도록 토큰 만료시간을 짧게 설정한다.
- 탈취가 의심되는 경우 해당 토큰을 강제로 만료시키면 된다.
- 접속기기 혹은 IP가 다른 경우 등
'CS ( Computer Science ) > 네트워크 (Networking)' 카테고리의 다른 글
[Net] Session & Cookie의 관계 (0) | 2024.12.31 |
---|---|
[Net] Token & JWT (0) | 2024.12.29 |
[Net] MVC 패턴 (0) | 2024.12.13 |
[Net] API 설계 (0) | 2024.12.12 |
[Net] Cookie / Session / Token / JWT / Filter (0) | 2024.12.12 |