Web 개발

쿠키, 세션, 세션ID 란? 그들의 차이점?

kk-nic 2024. 11. 13. 00:50

 

웹 개발을 하거나, 취약점 진단 학습을 시작 할 때, 

쿠키, 세션, 세션ID 라는 말을 들어봤을 것이다. 알아보자.

 

쿠키, 세션, 세션ID는 대략 뭐냐? 

사용자의 브라우저와 웹사이트의 서버가 자신의 상태를 알리고, 서로를 식별하고 만든 문자열이다.

 

애초에 왜 서로를 식별을 못해?

HTTP는 무상태 프로토콜(stateless)이다. 
즉, 각 요청은 독립적이고 이전 요청의 상태를 기억하지 않는다.
그래서 쿠키, 세션, 세션ID 는 상대방을 매번 요청 떄마다 식별하고, 이전 상태값을 기억 하려고 만든것이다.


쿠키란? 

서버측에서 "야 우리사이트에서 너(클라이언트 브라우저)를 식별할 수 있는 방법이 이거야" 라고 주는 고유한 문자열이다.

서버측에 저장 되지 않고, 클라이언트 측(메모리나 하드) 에 저장된다.

어떻게 동작하니? (쿠키 동작 매커니즘)

1.  A브라우저는 Z사이트에 최초 접속을 시도한다.

2.  Z사이트의 서버는 A브라우저에게 응답메시지의 Set-Cookie 헤더의 서버 자신이 생성한 세션ID(문자열) 을 준다.
3.  A브라우저는 받은 문자열은 메모리 또는 하드에 저장

4.  A브라우저는 Z사이트에게 요청을 할 때 마다, 요청메시지의 Cookie 라는 헤더를 포함해서 보낸다.(서버에게 받은 문자열을 보냄, 서버에게 "이게 나야" 라고 식별하라고 보냄)

 

쿠키의 종류(세션쿠키, 영구쿠기)

세션쿠키와 영구쿠기의 차이점은, 저장되는 위치와 얼마나 유지하는데에 있다.

세션 쿠키(Session Cookie) -> 메모리에 저장되고, 브라우저 종료 시 사라진다.

영구 쿠키(Persistent Cookie) -> 하드 디스크에 저장되며, SQLite DB 파일로 저장되는 경우가 많다.

 

세션쿠키와 영구쿠키를 누가 정할 수 있어? 누가 선택하는거야?

세션 쿠키(Session Cookie)와 영구 쿠키(Persistent Cookie)를 선택하는 주체는 서버 측 웹 애플리케이션 개발자 이다.

즉, 세션 쿠키와 영구 쿠키의 유형은 서버 측 개발자가 설정한다. Expires 또는 Max-Age 속성을 통해 쿠키의 만료 여부를 결정한다.

 

세션 쿠키 (Session Cookie)는 Expires 또는 Max-Age 속성이 없을 때 자동으로 세션 쿠키가 된다.

ex) Set-Cookie: sessionid=abc123xyz; Path=/; HttpOnly

 

영구 쿠키 (Persistent Cookie)는 Expires 또는 Max-Age 속성을 설정하면 영구 쿠키가 된다.

ex) Set-Cookie: username=admin; expires=Thu, 31 Dec 2024 23:59:59 GMT; Path=/; Secure

 

쿠키유형을 선택하는 것은 서버측 개발자 이지만,

클라이언트는 브라우저 설정을 통해 쿠키 저장 방식을 간접적으로 조정이 가능하다.

일반적으로 클라이언트(사용자,브라우저)는 쿠키의 유형을 직접 결정할 수 없다. 하지만 브라우저 설정에서 쿠키를 

허용/차단 하거나 브라우저 종료시 쿠키 삭제 옵션으로 선택할 수 있다.

예를 들어, Chrome에서는 " 모든 창이 닫히면 쿠키 및 사이트 데이터 삭제 " 옵션을 활성화하면 영구 쿠키도 삭제된다.

참고크롬설정 -> chrome://settings/content/siteData

 

영구쿠키는 하드 어디에 저장되?(쿠키 저장경로)

쿠키 파일의 경로는 브라우저와 운영체제에 따라 다르다.

브라우저 개발자 도구나 SQLite 브라우저로 쿠키 데이터를 쉽게 확인이 가능하다.

크롬의 경우 C:\Users\<username>\AppData\Local\Google\Chrome\User Data\Default\Network\Cookies 경로에 저장. 

이경로의 Cookies 파일을 노트패드나 메모장으로 열면, 제대로 확인이 불가능하다.(문자가 깨진 형태로 나온다)

 

어떻게 확인하니? (쿠키 데이터 확인 방법)

1. 브라우저 개발자 도구: Chrome, Firefox, Edge에서는 개발자 도구(F12 키)에서 Application 또는 Storage 탭에서 Cookies 항목을 선택해 현재 페이지의 쿠키를 볼 수 있다.

2. SQLite 브라우저: Cookies 또는 cookies.sqlite 파일을 SQLite DB Browser와 같은 도구로 열면, 저장된 쿠키 데이터를 직접 확인할 수 있다.

 

 

세션이란?

서버에 저장되는 정보 즉, 클라이언트들 정보이다.

 

세션ID 란?

저장된 세션을 식별하기위한 데이터를 말한다.

세션ID는 서버가 클라이언트 마다 생성하는 문자열이다. 

이 문자열로 "아 너는 A브라우저 구나?, 너는 B브라우저 구나?" 라고 식별한다.

 

세션ID 는 어떻게 동작하지? (세션ID 매커니즘)

1. A클라이언트가 Z사이트의  처음 접속

 

2.  Z사이트의 서버는 해당 A클라이언트를 식별할 수 있는 고유한 세션 ID를 생성한다.

 -> 세션 ID는 일반적으로 랜덤한 문자열로 생성되어 예측할 수 없게 한다. 타임스탬프(현재 시간), 랜덤 숫자, 또는 해시 알고리즘 등을 사용하여 생성

-> 추가로, 사용자의 데이터정보도 같이 저장한다.(때문에, 로그인정보나 장바구니 기능을 서버측에서 관리할 수 있음)

 

3.  생성한 세션 ID를 Z사이트 서버 내의 세션 저장소에 저장을 한다.

-> 세션ID 에는 사용자 

 -> 세션저장소는 OS마다 경로가 다다르고, OS하드에 저장할 수 도 있고, 데이터베이스에 저장할 수도 있다. 이건 개발자 마음.

 

4. Z사이트 서버가 응답메시지의 Set-Cookie 헤더로 A사이트 브라우저에게 세션 ID 전달

서버는 생성된 세션 ID를 클라이언트에 전달하기 위해 Set-Cookie 헤더를 사용하여 클라이언트의 웹 브라우저에 쿠키로 세션 ID를 보낸다.  이때, PHPSESSID와 같은 쿠키 이름을 사용하여 세션 ID를 전달한다.

ex) Set-Cookie: PHPSESSID=abc123xyz; Path=/; HttpOnly; Secure

 

 

5. A브라우저는 Z사이트 서버로부터 받은 세션 ID를 자신의 브라우저 쿠키에 저장. 이후 클라이언트는 같은 사이트에 접근할 때마다 이 세션 ID를 쿠키를 통해 서버에 전송한다.

 

6. 클라이언트가 후속 요청을 보낼 때, 저장된 세션 ID를 Cookie 헤더(즉, 쿠키)에 포함시켜 서버로 전송합니다.

ex) Cookie: PHPSESSID=abc123xyz

 

세션과 세션ID는 서버 어디에 저장되? (윈도우 XAMPP 기준)

세션ID가 저장되는 위치는 개발자가 정하는 것이다.

서버 내 디스크에 저장을 할지? 데이터베이스에 저장을 할지?

 

서버 내 저장을 할 경우는 OS마다 다르고, 또 사용하는 언어나 프레임워크마다 다르다. 

윈도우 11에 XAMPP를 설치하여 어디에 저장 되는지 확인을 해본 결과, C:\xampp\tmp 내 sess_로 시작하는 파일로 저장. 리눅스는 또 리눅스 내 저장하는 위치가 있을 것이다. (구글링 해보면 금방 나옴)

 

문제는 서버 내 저장 위치로 개발자가 정할 수 있다.

PHP의 경우 설정파일인 php.ini 파일 내 session.save_path 항목에 경로를 정의하면 그 곳에 세션정보가 파일형태로 쌓이게 된다. ex) session.save_path = "/var/lib/php/sessions"   ->  리눅스의 세션 저장 경로 예시

php.ini 파일에서 세션 관련 설정을 통해 세션 데이터 저장 위치, 세션 쿠키의 유효 기간, 세션 ID 생성 방식 등을 제어한다.

 

쿠키, 세션, 세션ID 매커니즘 총정리

 

1. A브라우저는 Z사이트에 최초 접근.(페이지 요청 메시지 전송)

 

2. Z사이트 서버는 접근 확인하고, 세션ID(ABC) 발급하여, 응답 메시지의 Set-cookie 헤더에 넣어 전송

 -> 이때, Z사이트 서버가 생성한 세션ID(ABC)는 서버 측에서 따로 저장하지 않는다. 

 -> A브라우저의 쿠키에만 저장 된다.

 

3.  A브라우저는 세션ID(ABC)를 쿠키값에 가지고 있는 상태에서 로그인을 시도한다. 

 

4. 로그인요청 메시지의 Cookie 헤더의 세션ID(ABC)를 넣어 Z사이트 서버로 보낸다.

 

5.  Z사이트 서버는 로그인이 성공이 되면, 세션ID(ABC)를 서버 내 특정 경로의 "sess_" 로 시작하는 파일명으로 저장한다.

-> sess_ 로 시작하는 파일 안에는 세션ID(ABC) 정보가 기록 되어있다.

(+) 다른 식별자와 함께 저장되어있다. (ex. $_SESSION 환경변수에 개발자가 기록한 키 값 ) 

 

6. 이후 A브라우저는 Z사이트의 다른 페이지에 접근을 할 때마다. 세션ID(ABC) 를 Cookie 헤더에 넣어 보내면, Z사이트 서버는 저장된 sess_~~ 파일로 식별하여, 로그인 되어 정상 접근을 하는 클라이언트로 인식하여, A브라우저는 서비스를 이용 할 수 있다.

 

자, 이제 실제로 어떻게 동작하는지 직접 구현해서, 확인해보자.

소스코드는 맨 아래 있음.

실습환경 : 윈도우 10 + XAMPP(apache + PHP + Mysql )

-> 크롬브라우저 : 클라이언트 역할 

-> XAMPP : 서버 역할 

 

페이지 흐름 : login.php --> login_process.php --> login_ok.php --> logout.php --> 다시 login.php

 

로그인 페이지 내 세션ID를 확인할 수 있도록 코딩.

 

1. 클라이언트가 최초 사이트 접근 시

1. 최초 접근

로그인 페이지에는 서버측에서 저장하고 있는 세션ID를 출력 할 수 있도록 코딩을 해놓았다.

최초 접근 시에는 서버측에서 저장하고 있는 세션ID는 없다. 

하지만 브라우저는 서버측으로 부터 받은 랜덤한 문자열의 세션ID를 받았기 때문에 확인이 가능하다 .

로그인 페이지에서 F12 개발자도구 -> Application -> Stroage -> Cookies -> 해당 사이트 클릭을 하면 서버측에서 받아온 세션ID가 확인이 된다. 

 

즉, 최초접근 시에는 서버는 세션ID를 생성해서 클라이언트에게 발급은 하지만, 서버에서는 따로 저장안하고 있다.

반면, 클라이언트는 받은 세션ID를 브라우저가 기억하고 있다(메모리 나 하드)

 

2. 없는 계정으로 로그인 시도

2. 없는 계정으로 로그인

 

 

3. 없는 계정으로 로그인 시도 결과. 

3. 로그인 결과

로그인을 판단하는 php파일은 login_process.php 파일이다.

실제로, login_process.php 에는 alert 으로 서버측에서 저장하고 있는 세션ID를 출력하도록 코딩했다.

로그인 실패 이기 때문에 당연히 서버측에서 저장하고 있는 세션ID는 없다.

 

 

4.두번째 접근

4. 두번째 접근

 

 login_process.php 파일에서 로그인 실패 시 login.php 로 포워딩 될 수 있도록 코딩했다.

그러니, 로그인 실패 시 알람이 발생하고 "확인" 버튼 클릭하면 알아서 login.php 로 돌아온다.

두번째 접근 시 에도 첫번째 접근 했을 때와 같은 세션ID를 브라우저는 가지고 있다.

반면 서버측에서는 저장 안하고 있는게 보인다.

 

 

4. 존재하는 계정으로 로그인 시도

5. 존재하는 계정으로 로그인 시도

 

 

6. 로그인 성공 후 세션ID 변화

6. 로그인 결과

login_process.php 에 나오는 알람은 서버측에서 저장하고 있는 세션ID를 출력하도록 했다.

로그인 성공/실패 여부는 판단하는 login_process.php 에서 로그인 성공 시 세션ID를 재발급 하지 않고, 

기존 세션ID를 저장 하도록 코딩 했다.  그렇기 떄문에, 기존의 브라우저가 가지고 있던 세션ID를 기반으로 서버는 저장한다. (로그인 성공 시 세션ID 재발급 하도록 코딩하면, 새로운 세션ID가 저장되고 출력된다.)

 

7. 로그인 후 세션 ID 정보 

7. 로그인 후 세션ID 정보 (로그 아웃 버튼)

로그인이 성공 하면 login_ok.php 로 포워딩 되는데, 이 페이지에서도 서버측에서 저장하고 있는 세션ID와 세션파일이 

저장되어있는경로는 출력하도록 만들었다.

 

브라우저는 클라이언트이고, 브라우저의 세션ID값과 

위 사진처럼 서버측에서 가져온 세션ID 값이 동일하다.

 

이 상태에서 "로그아웃" 버튼을 눌러보자.

 

8. 로그아웃 후 세션ID 변화

8. 로그아웃 이후 세션ID

로그아웃 버튼을 누르면 logout.php 파일로 이동하여 세션과 세션ID를 삭제하고 login.php 로 포워딩 될 수 있도록

만들었다.

그렇기 떄문에 크롬 브라우저의 세션ID 값이 이전의 세션ID 값과 다른 것을 볼 수 있다.

서버측에서 로그아웃 된 상태에서 클라이언트가 다시 접근하니, 새로운 세션ID를 발급 해준것.

실제로, 왼쪽 세션 ID :  항목을 보면, 서버측에서 저장하고 있는 세션ID는 존재하지 않는 것을 볼 수 있다.

 

자 그러면, 

세션ID는 언제? 어떻게 저장이 되는지 확인해보자.

다시  계정: user / 비번 : user  로 로그인을 해서 서버측에서 저장하고 있는 세션 ID를 확인해보자.

 

브라우저는 세션ID s9j1l0an8353hah2ce646ale47 를 서버측에서 받아왔다.

로그인을 하면, 서버는 s9j1l0an8353hah2ce646ale47 를 sess_ 로 시작하는 파일로 저장을 한다고 했다.

또, XAMPP는 세션ID를 C:\xampp\tmp 아래 저장한다고 했다. 확인해보자.

 

 

9. 로그인 후 세션ID 저장경로

로그인 후 접근 가능한, login_ok.php 화면이다.

아래는 C:\xampp\tmp 내 생성 된 세션파일이다.

 

sess_s9j1l0an8353hah2ce646ale47 ->  sess_ 로 시작하여 뒤에는 세션ID 문자열이 오는 파일

 

id|s:4:"user";

id -> $_SESSION  세션 변수(환경변수)에 id 라는 키값을 의미한다. ex) $_SESSION['id']

s -> 문자열을 의미 

4 -> 문자열의 길이

"user" ->  $_SESSION 세션 변수의 'id'  key 의 value 를 의미한다. ex) $_SESSION['id'] = 계정명

 

이상태에서 로그아웃 버튼을 누르면 세션ID가 사라지게 코딩을 해놓았기 때문에

실제 C:\xampp\tmp\ sess_s9j1l0an8353hah2ce646ale47 파일가 삭제 된다(실제 해봄)

 

정리) 

- 쿠키는 클라이언트(브라우저)가 가지고 있는 문자열이다.(서버에게 자신을 식별 시키기 위한 문자열) 

- 세션과 세션ID는 서버측에서 클라이언트를 식별하기 위해서 존재하는 파일과 문자열이다.

- 세션은 클라이언정보를 담고 있는 파일정보를 의미하며, 세션ID는 서버측에서 생성하는 랜덤한 문자열이다. 

- 사용자가 사이트에 최초 접근 시 서버는 생성한 세션ID를 사용자에게 전달 할 뿐, 내부적으로 저장 하지 않는다.

- 로그인 성공되면, 세션ID를 개발자가 정해놓은 세션 키값 문자열에 따라서 저장한다. 

- 각 OS 마다 플랫폼 마다 세션정보를 저장해놓은 곳은 분명 존재한다. 

 

소스코드

login.php

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>로그인</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
    
</head>
<body>
<div class="container mt-5">

<main class="mx-auto w-75 border border-secondary rounded-2 p-5 " >
<form method="POST" class="w-75 m-auto" action="login_process.php">
    <h1 class="h3 mb-3">로그인</h1>
    <div class="form-floating mt-2">
        <input type="text" class="form-control" name="id" id="id" placeholder="" autocomplete="off">
        <label for="id">아이디</label>
    </div>

    <div class="form-floating mt-2">
        <input type="password" class="form-control" name="pw" id="pw" placeholder="">
        <label for="pw">패스워드</label>
    </div>
    <button class="w-100 btn btn-primary btn-lg mt-2" id="btn_login" type="submit">로그인</button>
</form>
</main>
</div>
<div class="container">
    <main class="mx-auto w-50 mt-2 border border-secondary rounded-2 p-5 " >
    <?php 
        // 세션 ID 출력
        echo "세션 ID: " . session_id(); 

        // 현재 세션 파일 확인
        echo "<br>세션 파일 경로: " . session_save_path();
    ?>
    </main>
</div>
</body>
</html>

 

 

login.process.php

<?php
// 데이터베이스 연결 정보
$servername = "localhost";
$username = "root"; 
$password = "";    
$dbname = "login";

// 데이터베이스 연결
$conn = mysqli_connect($servername, $username, $password, $dbname);

$id = (isset($_POST['id']) && $_POST['id']!='') ? $_POST['id'] : $_POST['id']='';
$pw = (isset($_POST['pw']) && $_POST['pw']!='') ? $_POST['pw'] : $_POST['pw']='';

if (!$conn){
    die("DB와 연결 실패 :". mysqli_connect_error()."<br>");
}

try{
    $sql = "SELECT * FROM `login` WHERE username = '{$id}'"; 
    $result = mysqli_query($conn, $sql);
    if (!$result) {
        die("Query failed: " . mysqli_error($conn));
    }
    $row = mysqli_fetch_assoc($result);

    if($result == '') {
        echo '제대로 입력하세요.';
        exit;
    }

    if (mysqli_num_rows($result) > 0) { // 로그인 성공 시
        session_start();

        $_SESSION['id'] = $row['username'];  // 세션변수 id키 의 value로 계정명 넣기
        echo '<script>
        alert(\'세션ID:'.session_id().'\');
        location.href=\'./login_ok.php\'
        </script>';
        
        mysqli_close($conn);

    } else {
        echo '<script>alert("아이디 또는 비밀번호가 틀렸습니다. \n 세션ID: '.session_id().'"); location.href=\'./login1.php\'</script>';
        // echo '<script>alert("아이디 또는 비밀번호가 틀렸습니다. \n SQL : '.$sql.'"); location.href=\'./login1.php\'</script>';
        mysqli_close($conn);
    }

} catch (mysqli_sql_exception $e) {
    echo $e->getMessage();
}

?>

 

 

login_ok.php

<?php 
session_start();
if(isset($_SESSION['id']) == ''){
    echo '<script>alert("돌아가라.");  location.href = "./login1.php";</script>';
}else{
    echo '<script>alert("로그인 성공")</script>';
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <button id="logout_button">로그아웃</button>
    <?php 
    echo "세션 ID: " . session_id(); // 세션 ID 출력

    // 현재 세션 파일 확인
    echo "<br>세션 파일 경로: " . session_save_path();
    ?>
    <script>
        var logout_button = document.querySelector('#logout_button')
        logout_button.addEventListener("click", () => {
            window.location.href = './logout.php';
        })
    </script>
</body>
</html>

 

 

logout.php

<?php
session_start(); // 세션 시작

// 세션 ID 재발급
session_regenerate_id(true);

// 모든 세션 변수 제거
session_unset();

// 세션 종료
session_destroy();

// 로그아웃 후 리다이렉트 (로그인 페이지로 이동)
header("Location: login1.php");
exit;
?>

 

 

sql

CREATE DATABASE login; # login 데이터베이스 생성 

use login; # login 데이터베이스 사용 지정 

CREATE TABLE `login`(   # login 테이블 생성 
`idx` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`username` VARCHAR(255) NOT NULL,
`password` VARCHAR(255) NOT NULL,
PRIMARY KEY(idx)
);

INSERT INTO `login` (`username`, `password`) VALUES ('user', 'user'); # 데이터 삽입 

select * from login; # login 테이블 전체 조회