CS ( Computer Science )/네트워크 (Networking)

[Net] Cookie

JABHACK 2024. 12. 28. 20:40

Cookie

📌 사용자의 웹 브라우저에 저장되는 정보로 사용자의 상태 혹은 세션을 유지하거나 사용자 경험을 개선하기 위해 사용된다. 사용자 정보나 세션 데이터를 클라이언트(브라우저)에 저장하는 기술

  • Cookie는 주로 사용자 세션 관리(로그인, 장바구니, 접속시간)나 광고 트래킹(사용자 행동) 등의 목적으로 사용된다.

  1. HTTP는 Stateless, Connectionless 특성을 가지고 있다.
  2. Client가 재요청시 Server는 이전 요청에 대한 정보를 기억하지 못한다.
  3. 로그인과 같이 상태를 유지해야 하는 경우가 발생한다.
  4. Request에 사용자 정보를 포함하면 해결이 된다.
    • 로그인 후에는 사용자 정보와 관련된 값이 저장되어 있어야한다.
  5. 브라우저를 완전히 종료한 뒤 다시 열어도 사용자 정보가 유지되어야 한다.
서버에 전송하지 않고 브라우저에 단순히 데이터를 저장하고 싶다면 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

  1. Set-Cookie
    • Server에서 Client로 Cookie 전달(Response Header)
  2. 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의 생명주기
    1. 세션 Cookie
      • 만료 날짜를 생략하면 브라우저 완전 종료시 까지만 유지된다.(Default)
        • expires, max-age 가 생략된 경우
      • 브라우저를 완전 종료 후 다시 페이지를 방문했을 때 다시 로그인을 해야한다.
    2. 영속 Cookie
      • 만료 날짜를 입력하면 해당 날짜까지 유지한다.
        • expires=Sat, 11-Dec-2024 00:00:00 GMT;
          • 해당 만료일이 도래하면 쿠키가 삭제된다.
        • max-age=3600 (second, 3600초는 한시간. 60 * 60)
          • 0이 되거나 음수를 지정하면 쿠키가 삭제된다.
  • Cookie의 도메인
    • 쿠키가 아무 사이트에서나 생기고 동작하면 안된다!
      • 필요없는 값 전송, 트래픽 문제 등이 발생한다.
    • domain=spartacodingclub.kr
      • domain=spartacodingclub.kr를 지정하여 쿠키를 저장한다.
      • dev.spartacodingclub.kr와 같은 서브 도메인에서도 쿠키에 접근한다.
    • domain을 생략하면 현재 문서 기준 도메인만 적용한다.
  • Cookie의 경로
    • 1차적으로 도메인으로 필터링 후 Path가 적용된다.
    • 일반적으로 path=/ 루트(전체)로 지정한다.
    • 위 경로를 포함한 하위 경로 페이지만 쿠키에 접근한다.
      • path=/api 지정
        • path=/api/example 가능
        • path=/example 불가능
  • Cookie 보안
    1. Secure
      • 기본적으로 Cookie는 http, https 구분하지 않고 전송한다.
      • Secure를 적용하면 https인 경우에만 전송한다. s = Secure
    2. HttpOnly
      • XSS(Cross-site Scripting) 공격을 방지한다.
        • 악성 스크립트를 웹 페이지에 삽입하여 다른 사용자의 브라우저에서 실행되도록 하는 공격
      • 자바스크립트에서 Cookie 접근을 못하도록 막는다.
      • HTTP 요청시 사용한다.
    3. SameSite
      • 비교적 최신 기능이라 브라우저 지원여부를 확인 해야한다.
      • CSRF(Cross-Site Request Forgery) 공격을 방지한다.
        • 사용자가 의도하지 않은 상태에서 특정 요청을 서버에 전송하게 하여 사용자 계정에서 원치 않는 행동을 하게 만든다.
      • 요청 도메인과 쿠키에 설정된 도메인이 같은 경우만 쿠키 전송

 

 

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

}
  1. 로그인 기능
    • 로그인에 성공하면 Cookie를 생성하고 HttpServletResponse 객체에 담는다.
      • Cookie 이름(Key)은 userId , 값(Value)은 회원 index 값을 담아둔다.
      • Set-Cookie: userId=1
    • 만료 시간을 지정하지 않으면 세션 쿠키로 만들어진다.
      • 브라우저 종료 전까지 userId 가 모든 요청 헤더의 Cookie에 담겨서 전달된다.
  2. 로그아웃 기능
    • 새로운 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를 사용하는 이유 중 하나에 속한다.
    • 민감한 정보를 저장하면 안된다.
  • 한번 탈취된 정보는 변경이 없다면 반영구적으로 사용할 수 있다.

 

보안 대처방법

  1. 쿠키에 중요한값을 저장하지 않는다.
  2. 사용자 별로 일반 유저나 해커들이 알아보지 못하는 값을 노출한다.
    • 일반적으로 암호화된 Token을 쿠키에 저장한다.
    • 서버에서 암호화된 Token과 사용자를 매핑해서 인식한다.
    • Token은 서버에서 관리한다.
  3. 토큰은 해커가 임의의 값을 넣어도 동작하지 않도록 만들어야 한다.
  4. 해커가 토큰을 탈취해도 사용할 수 없도록 토큰 만료시간을 짧게 설정한다.
  5. 탈취가 의심되는 경우 해당 토큰을 강제로 만료시키면 된다.
    • 접속기기 혹은 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