跳轉到

Cookie 與 Session

開始之前

任務目標

在這個章節中,我們會完成:

  • 了解什麼是 Cookie
  • 學習如何在 Django 中操作 Cookie
  • 認識 Cookie 的優缺點與使用場景
  • 了解什麼是 Session
  • 學習如何在 Django 中操作 Session
  • 認識 Session 的優缺點與使用場景
  • 比較 Cookie 與 Session 的差異

Cookie 是一種存在於瀏覽器端的小型文字資料,由伺服器傳送給瀏覽器,瀏覽器會將它儲存起來,並在後續的請求中自動帶上。

sequenceDiagram
    participant Browser as 瀏覽器
    participant Server as 伺服器

    Browser->>Server: 第一次訪問網站
    Server->>Browser: Set-Cookie: username=Django
    Note over Browser: 儲存 Cookie
    Browser->>Server: 第二次訪問網站 (帶 Cookie)
    Note over Browser: Cookie: username=Django
    Server->>Browser: 回應(知道你是 Django)

Cookie 的特性

  1. 儲存在瀏覽器:資料存在使用者的電腦上
  2. 自動傳送:每次請求都會自動帶上
  3. 有大小限制:單個 Cookie 最大約 4KB
  4. 有期限:可以設定過期時間
  5. 有數量限制:每個網域約 50-150 個 Cookie

Cookie 常用於:

  • 記住使用者偏好:語言、主題、字體大小
  • 購物車:暫存購物車內容
  • 記住登入狀態:「記住我」功能
  • 追蹤分析:Google Analytics、廣告追蹤

讓我們透過實作來學習如何操作 Cookie。

範例 1:記錄訪問次數

我們將建立一個頁面,記錄使用者訪問了幾次。

建立 View

編輯 practices/views.py,加入以下程式碼:

practices/views.py
from django.http import HttpResponse
from django.shortcuts import render


def hello_world(request):
    return render(request, "practices/hello.html")


def greeting(request):
    name = "Django"
    return render(request, "practices/greeting.html", {"name": name})


def search(request):
    keyword = request.GET.get("q", "")
    return render(request, "practices/search.html", {"keyword": keyword})


def product_list(request):
    category = request.GET.get("category", "all")
    sort = request.GET.get("sort", "newest")
    page = request.GET.get("page", "1")

    return HttpResponse(f"分類: {category}, 排序: {sort}, 頁數: {page}")


def filter_products(request):
    colors = request.GET.getlist("color")
    return HttpResponse(f"選擇的顏色: {', '.join(colors)}")


def hello_name(request, name):
    return HttpResponse(f"Hello, {name}!")


def article_detail(request, year, month, slug):
    return HttpResponse(f"文章: {year}{month} 月 - {slug}")


def user_articles(request, username):
    sort = request.GET.get("sort", "newest")
    page = request.GET.get("page", "1")

    return HttpResponse(f"{username} 的文章, 排序: {sort}, 頁數: {page}")


def advanced_search(request):
    keyword = request.GET.get("q", "")
    category = request.GET.get("category", "all")
    sort = request.GET.get("sort", "newest")

    return render(
        request,
        "practices/advanced_search.html",
        {
            "keyword": keyword,
            "category": category,
            "sort": sort,
        },
    )


def color_filter(request):
    colors = request.GET.getlist("color")
    return render(
        request,
        "practices/color_filter.html",
        {"colors": colors},
    )


def contact(request):
    context = {}

    if request.method == "POST":
        name = request.POST.get("name", "")
        email = request.POST.get("email", "")
        message = request.POST.get("message", "")

        context = {
            "success": True,
            "name": name,
            "email": email,
            "message": message,
        }

    return render(request, "practices/contact.html", context)


def cookie_counter(request):  # (1)!
    # 從 Cookie 讀取訪問次數
    visit_count = request.COOKIES.get("visit_count", "0")  # (2)!
    visit_count = int(visit_count) + 1  # (3)!

    # 建立回應
    response = HttpResponse(f"你已經訪問了 {visit_count} 次")

    # 設定 Cookie
    response.set_cookie("visit_count", str(visit_count))  # (4)!

    return response
  1. 建立一個新的 view 來記錄訪問次數
  2. request.COOKIES 讀取 Cookie,預設值為 "0"
  3. 將字串轉為整數並加 1
  4. 使用 response.set_cookie() 設定 Cookie

程式碼重點

  1. 讀取 Cookierequest.COOKIES.get("key", "default")
  2. 設定 Cookieresponse.set_cookie("key", value)
  3. Cookie 值都是字串:需要手動轉換型別

設定 URL

編輯 practices/urls.py,加入 URL 路由:

practices/urls.py
from django.urls import path

from practices import views

app_name = "practices"

urlpatterns = [
    path("hello/", views.hello_world, name="hello_world"),
    path("greeting/", views.greeting, name="greeting"),
    path("search/", views.search, name="search"),
    path("products/", views.product_list, name="product_list"),
    path("products/filter/", views.filter_products, name="product_filter"),
    path("hello/<str:name>/", views.hello_name, name="hello_name"),
    path(
        "articles/<int:year>/<int:month>/<slug:slug>/",
        views.article_detail,
        name="article_detail",
    ),
    path(
        "users/<str:username>/articles/",
        views.user_articles,
        name="user_articles",
    ),
    path("advanced-search/", views.advanced_search, name="advanced_search"),
    path("color-filter/", views.color_filter, name="color_filter"),
    path("contact/", views.contact, name="contact"),
    path("cookie-counter/", views.cookie_counter, name="cookie_counter"),
]

測試

啟動開發伺服器:

uv run manage.py runserver

訪問 http://127.0.0.1:8000/practices/cookie-counter/

  • 第一次訪問:「你已經訪問了 1 次」
  • 第二次訪問:「你已經訪問了 2 次」
  • 第三次訪問:「你已經訪問了 3 次」

開發者工具查看 Cookie

在瀏覽器中按 F12 開啟開發者工具:

  1. 切換到「Application」或「儲存空間」頁籤
  2. 左側選擇「Cookies」
  3. 選擇網站網址
  4. 可以看到 visit_count Cookie

範例 2:記住使用者偏好

讓我們建立一個更實用的範例:記住使用者選擇的主題顏色。

建立 View

編輯 practices/views.py,加入以下程式碼:

practices/views.py
from django.http import HttpResponse
from django.shortcuts import render


def hello_world(request):
    return render(request, "practices/hello.html")


def greeting(request):
    name = "Django"
    return render(request, "practices/greeting.html", {"name": name})


def search(request):
    keyword = request.GET.get("q", "")
    return render(request, "practices/search.html", {"keyword": keyword})


def product_list(request):
    category = request.GET.get("category", "all")
    sort = request.GET.get("sort", "newest")
    page = request.GET.get("page", "1")

    return HttpResponse(f"分類: {category}, 排序: {sort}, 頁數: {page}")


def filter_products(request):
    colors = request.GET.getlist("color")
    return HttpResponse(f"選擇的顏色: {', '.join(colors)}")


def hello_name(request, name):
    return HttpResponse(f"Hello, {name}!")


def article_detail(request, year, month, slug):
    return HttpResponse(f"文章: {year}{month} 月 - {slug}")


def user_articles(request, username):
    sort = request.GET.get("sort", "newest")
    page = request.GET.get("page", "1")

    return HttpResponse(f"{username} 的文章, 排序: {sort}, 頁數: {page}")


def advanced_search(request):
    keyword = request.GET.get("q", "")
    category = request.GET.get("category", "all")
    sort = request.GET.get("sort", "newest")

    return render(
        request,
        "practices/advanced_search.html",
        {
            "keyword": keyword,
            "category": category,
            "sort": sort,
        },
    )


def color_filter(request):
    colors = request.GET.getlist("color")
    return render(
        request,
        "practices/color_filter.html",
        {"colors": colors},
    )


def contact(request):
    context = {}

    if request.method == "POST":
        name = request.POST.get("name", "")
        email = request.POST.get("email", "")
        message = request.POST.get("message", "")

        context = {
            "success": True,
            "name": name,
            "email": email,
            "message": message,
        }

    return render(request, "practices/contact.html", context)


def cookie_counter(request):
    # 從 Cookie 讀取訪問次數
    visit_count = request.COOKIES.get("visit_count", "0")
    visit_count = int(visit_count) + 1

    # 建立回應
    response = HttpResponse(f"你已經訪問了 {visit_count} 次")

    # 設定 Cookie
    response.set_cookie("visit_count", str(visit_count))

    return response


def theme_preference(request):  # (1)!
    # 從 GET 參數讀取主題設定
    theme = request.GET.get("theme")  # (2)!

    # 從 Cookie 讀取目前的主題
    current_theme = request.COOKIES.get("theme", "light")  # (3)!

    # 如果有新的主題設定就更新
    if theme:  # (4)!
        current_theme = theme

    # 建立回應
    response = render(request, "practices/theme.html", {"theme": current_theme})

    # 儲存主題設定到 Cookie
    if theme:  # (5)!
        response.set_cookie("theme", current_theme, max_age=365 * 24 * 60 * 60)  # (6)!

    return response
  1. 建立主題偏好設定的 view
  2. 從 GET 參數讀取使用者選擇的主題
  3. 從 Cookie 讀取目前的主題,預設為 "light"
  4. 如果有新的主題設定,就更新
  5. 只有在使用者選擇新主題時才更新 Cookie
  6. 設定 Cookie 有效期為 1 年(365 天)

set_cookie 的參數

set_cookie() 常用參數:

  • key:Cookie 的名稱
  • value:Cookie 的值
  • max_age:有效期限(秒),例如:365 * 24 * 60 * 60 代表 1 年
  • expires:過期時間(datetime 物件)
  • path:Cookie 的有效路徑,預設為 /
  • domain:Cookie 的有效網域
  • secure:是否只在 HTTPS 連線中傳送
  • httponly:是否禁止 JavaScript 存取

建立 Template

建立 practices/templates/practices/theme.html

practices/templates/practices/theme.html
<!DOCTYPE html>
<html lang="zh-Hant">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="This is my first Django Template" />
    <meta name="keywords" content="Django, Template, HTML" />
    <title>主題設定</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        padding: 20px;
        transition: all 0.3s;
      }

      /* 淺色主題 */
      body.light {
        background-color: #ffffff;
        color: #333333;
      }

      /* 深色主題 */
      body.dark {
        background-color: #1a1a1a;
        color: #f0f0f0;
      }

      .container {
        max-width: 600px;
        margin: 0 auto;
      }

      h1 {
        margin-bottom: 30px;
      }

      .theme-buttons {
        display: flex;
        gap: 10px;
        margin-bottom: 20px;
      }

      button {
        padding: 10px 20px;
        border: 2px solid currentColor;
        border-radius: 5px;
        background-color: transparent;
        color: inherit;
        cursor: pointer;
        font-size: 16px;
      }

      button:hover {
        background-color: currentColor;
        color: white;
      }

      .current-theme {
        padding: 15px;
        border-left: 4px solid currentColor;
        background-color: rgba(128, 128, 128, 0.1);
      }
    </style>
  </head>
  <body class="{{ theme }}">
    <div class="container">  <!-- (1)! -->
      <h1>
        主題設定
      </h1>

      <div class="theme-buttons">
        <a href="?theme=light">  <!-- (2)! -->
          <button>
            淺色主題
          </button>
        </a>
        <a href="?theme=dark">  <!-- (3)! -->
          <button>
            深色主題
          </button>
        </a>
      </div>

      <div class="current-theme">
        <p>
          目前使用:<strong>{{ theme }}</strong> 主題
        </p>
        <p>  <!-- (4)! -->
          你的偏好已儲存在 Cookie 中,下次訪問時會自動套用。
        </p>
      </div>
    </div>
  </body>
</html>
  1. 根據主題設定不同的 class
  2. 點擊後會傳送 ?theme=light 參數
  3. 點擊後會傳送 ?theme=dark 參數
  4. 顯示目前的主題

設定 URL

編輯 practices/urls.py

practices/urls.py
from django.urls import path

from practices import views

app_name = "practices"

urlpatterns = [
    path("hello/", views.hello_world, name="hello_world"),
    path("greeting/", views.greeting, name="greeting"),
    path("search/", views.search, name="search"),
    path("products/", views.product_list, name="product_list"),
    path("products/filter/", views.filter_products, name="product_filter"),
    path("hello/<str:name>/", views.hello_name, name="hello_name"),
    path(
        "articles/<int:year>/<int:month>/<slug:slug>/",
        views.article_detail,
        name="article_detail",
    ),
    path(
        "users/<str:username>/articles/",
        views.user_articles,
        name="user_articles",
    ),
    path("advanced-search/", views.advanced_search, name="advanced_search"),
    path("color-filter/", views.color_filter, name="color_filter"),
    path("contact/", views.contact, name="contact"),
    path("cookie-counter/", views.cookie_counter, name="cookie_counter"),
    path("theme/", views.theme_preference, name="theme_preference"),
]

測試

訪問 http://127.0.0.1:8000/practices/theme/

  1. 點擊「深色主題」按鈕
  2. 頁面變成深色背景
  3. 關閉瀏覽器並重新開啟
  4. 再次訪問 http://127.0.0.1:8000/practices/theme/
  5. 頁面自動顯示深色主題(Cookie 記住了你的選擇)

Cookie 成功運作

現在你已經學會:

  • ✅ 讀取 Cookie:request.COOKIES.get("key")
  • ✅ 設定 Cookie:response.set_cookie("key", value)
  • ✅ 設定過期時間:max_age=365 * 24 * 60 * 60

優點

Cookie 的優勢

  1. 實作簡單:瀏覽器自動處理儲存和傳送
  2. 適合小資料:適合儲存簡單的偏好設定
  3. 減輕伺服器負擔:資料存在使用者端
  4. 可設定期限:彈性控制資料有效期

缺點

Cookie 的限制

  1. 容量限制:單個 Cookie 約 4KB
  2. 安全性問題
    • 可以被使用者修改
    • 可能被 XSS 攻擊竊取(如果沒有設定 httponly
    • 在 HTTP 連線中會以明文傳送(除非使用 HTTPS)
  3. 隱私問題:追蹤 Cookie 引發隱私爭議
  4. 效能影響:每次請求都會傳送所有 Cookie

適合的場景

  • 使用者偏好設定:語言、主題、字體大小
  • 追蹤和分析:Google Analytics
  • 廣告投放:追蹤使用者興趣
  • 簡單的狀態記錄:訪問次數、上次訪問時間

不適合的場景

  • 敏感資料:密碼、信用卡號碼(容易被竊取)
  • 大量資料:超過 4KB 的資料
  • 需要安全性的資料:使用者可以隨意修改 Cookie
  • 跨裝置同步:Cookie 只存在單一瀏覽器

什麼是 Session?

Session 是一種存在於伺服器端的資料儲存機制,用來追蹤使用者的狀態。與 Cookie 不同,Session 的資料存在伺服器上,瀏覽器只會收到一個 Session ID。

Session 的運作原理

sequenceDiagram
    participant Browser as 瀏覽器
    participant Server as 伺服器
    participant DB as Session 儲存

    Browser->>Server: 第一次訪問
    Server->>DB: 建立 Session
    Note over DB: session_id: abc123<br/>data: {}
    Server->>Browser: Set-Cookie: sessionid=abc123
    Note over Browser: 儲存 Session ID

    Browser->>Server: 第二次訪問 (帶 Session ID)
    Note over Browser: Cookie: sessionid=abc123
    Server->>DB: 根據 Session ID 讀取資料
    DB-->>Server: 回傳 Session 資料
    Server->>Browser: 回應

Session 的特性

  1. 資料存在伺服器:更安全,使用者無法直接修改
  2. 容量更大:沒有 4KB 限制
  3. 使用 Session ID:瀏覽器只儲存一個 ID(透過 Cookie)
  4. 有過期機制:可設定閒置多久後失效
  5. 需要儲存空間:使用資料庫或快取來儲存

Session 的儲存方式

Django 支援多種 Session 儲存方式:

儲存方式 說明 優點 缺點
資料庫 預設方式,存在 django_session 可靠、持久化 較慢
快取 存在記憶體快取(如 Redis) 快速 伺服器重啟後消失
檔案 存在檔案系統 簡單 I/O 負擔
Cookie 資料加密後存在 Cookie 不需要伺服器儲存 有容量限制

Django 預設使用資料庫

Django 預設使用資料庫儲存 Session,資料會存在 django_session 表中。這是最可靠的方式,適合大多數情況。

在 Django 中操作 Session

範例 3:購物車功能

讓我們使用 Session 來實作一個簡單的購物車。

建立 View

編輯 practices/views.py

practices/views.py
from django.http import HttpResponse
from django.shortcuts import render


def hello_world(request):
    return render(request, "practices/hello.html")


def greeting(request):
    name = "Django"
    return render(request, "practices/greeting.html", {"name": name})


def search(request):
    keyword = request.GET.get("q", "")
    return render(request, "practices/search.html", {"keyword": keyword})


def product_list(request):
    category = request.GET.get("category", "all")
    sort = request.GET.get("sort", "newest")
    page = request.GET.get("page", "1")

    return HttpResponse(f"分類: {category}, 排序: {sort}, 頁數: {page}")


def filter_products(request):
    colors = request.GET.getlist("color")
    return HttpResponse(f"選擇的顏色: {', '.join(colors)}")


def hello_name(request, name):
    return HttpResponse(f"Hello, {name}!")


def article_detail(request, year, month, slug):
    return HttpResponse(f"文章: {year}{month} 月 - {slug}")


def user_articles(request, username):
    sort = request.GET.get("sort", "newest")
    page = request.GET.get("page", "1")

    return HttpResponse(f"{username} 的文章, 排序: {sort}, 頁數: {page}")


def advanced_search(request):
    keyword = request.GET.get("q", "")
    category = request.GET.get("category", "all")
    sort = request.GET.get("sort", "newest")

    return render(
        request,
        "practices/advanced_search.html",
        {
            "keyword": keyword,
            "category": category,
            "sort": sort,
        },
    )


def color_filter(request):
    colors = request.GET.getlist("color")
    return render(
        request,
        "practices/color_filter.html",
        {"colors": colors},
    )


def contact(request):
    context = {}

    if request.method == "POST":
        name = request.POST.get("name", "")
        email = request.POST.get("email", "")
        message = request.POST.get("message", "")

        context = {
            "success": True,
            "name": name,
            "email": email,
            "message": message,
        }

    return render(request, "practices/contact.html", context)


def cookie_counter(request):
    # 從 Cookie 讀取訪問次數
    visit_count = request.COOKIES.get("visit_count", "0")
    visit_count = int(visit_count) + 1

    # 建立回應
    response = HttpResponse(f"你已經訪問了 {visit_count} 次")

    # 設定 Cookie
    response.set_cookie("visit_count", str(visit_count))

    return response


def theme_preference(request):
    # 從 GET 參數讀取主題設定
    theme = request.GET.get("theme")

    # 從 Cookie 讀取目前的主題
    current_theme = request.COOKIES.get("theme", "light")

    # 如果有新的主題設定就更新
    if theme:
        current_theme = theme

    # 建立回應
    response = render(request, "practices/theme.html", {"theme": current_theme})

    # 儲存主題設定到 Cookie
    if theme:
        response.set_cookie("theme", current_theme, max_age=365 * 24 * 60 * 60)

    return response


def shopping_cart(request):  # (1)!
    # 取得操作類型
    action = request.GET.get("action")  # (2)!
    product = request.GET.get("product")

    # 從 Session 取得購物車, 如果沒有就建立空字典
    cart = request.session.get("cart", {})  # (3)!

    # 處理加入購物車
    if action == "add" and product:  # (4)!
        cart[product] = cart.get(product, 0) + 1  # (5)!
        request.session["cart"] = cart  # (6)!

    # 處理移除商品
    elif action == "remove" and product:  # (7)!
        if product in cart:
            del cart[product]
            request.session["cart"] = cart

    # 處理清空購物車
    elif action == "clear":  # (8)!
        request.session["cart"] = {}

    # 重新取得購物車, 可能已更新
    cart = request.session.get("cart", {})

    return render(request, "practices/cart.html", {"cart": cart})
  1. 建立購物車 view
  2. 從 GET 參數取得操作類型(add/remove/clear)
  3. 從 Session 取得購物車資料,預設為空字典
  4. 處理「加入購物車」操作
  5. 如果商品已存在,數量加 1;否則設為 1
  6. 更新 Session 中的購物車
  7. 處理「移除商品」操作
  8. 處理「清空購物車」操作

Session 操作重點

  1. 讀取 Sessionrequest.session.get("key", default)
  2. 寫入 Sessionrequest.session["key"] = value
  3. 刪除 Sessiondel request.session["key"]
  4. 自動儲存:Django 會自動處理 Session 的儲存

建立 Template

建立 practices/templates/practices/cart.html

practices/templates/practices/cart.html
<!DOCTYPE html>
<html lang="zh-Hant">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="This is my first Django Template" />
    <meta name="keywords" content="Django, Template, HTML" />
    <title>購物車</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        max-width: 800px;
        margin: 0 auto;
        padding: 20px;
      }

      h1 {
        color: #333;
      }

      .products {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
        gap: 10px;
        margin-bottom: 30px;
      }

      .product-btn {
        padding: 15px;
        background-color: #007bff;
        color: white;
        text-decoration: none;
        border-radius: 5px;
        text-align: center;
        display: block;
      }

      .product-btn:hover {
        background-color: #0056b3;
      }

      .cart {
        background-color: #f8f9fa;
        padding: 20px;
        border-radius: 5px;
      }

      .cart-item {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 10px;
        border-bottom: 1px solid #ddd;
      }

      .remove-btn {
        background-color: #dc3545;
        color: white;
        padding: 5px 10px;
        text-decoration: none;
        border-radius: 3px;
        font-size: 14px;
      }

      .remove-btn:hover {
        background-color: #c82333;
      }

      .clear-btn {
        background-color: #6c757d;
        color: white;
        padding: 10px 20px;
        text-decoration: none;
        border-radius: 5px;
        display: inline-block;
        margin-top: 10px;
      }

      .clear-btn:hover {
        background-color: #5a6268;
      }

      .empty-cart {
        color: #999;
        text-align: center;
        padding: 20px;
      }
    </style>
  </head>
  <body>
    <h1>
      購物車範例
    </h1>

    <h2>
      商品列表
    </h2>
    <div class="products">
      <a href="?action=add&product=蘋果" class="product-btn">加入 蘋果</a>
      <a href="?action=add&product=香蕉" class="product-btn">加入 香蕉</a>
      <a href="?action=add&product=橘子" class="product-btn">加入 橘子</a>
      <a href="?action=add&product=葡萄" class="product-btn">加入 葡萄</a>
    </div>

    <h2>
      購物車內容
    </h2>
    <div class="cart">
      {% if cart %}
        {% for product, quantity in cart.items %}
          <div class="cart-item">
            <span>{{ product }} x {{ quantity }}</span>
            <a href="?action=remove&product={{ product }}" class="remove-btn">移除</a>
          </div>
        {% endfor %}
        <a href="?action=clear" class="clear-btn">清空購物車</a>
      {% else %}
        <div class="empty-cart">
          購物車是空的
        </div>
      {% endif %}
    </div>
  </body>
</html>

設定 URL

編輯 practices/urls.py

practices/urls.py
from django.urls import path

from practices import views

app_name = "practices"

urlpatterns = [
    path("hello/", views.hello_world, name="hello_world"),
    path("greeting/", views.greeting, name="greeting"),
    path("search/", views.search, name="search"),
    path("products/", views.product_list, name="product_list"),
    path("products/filter/", views.filter_products, name="product_filter"),
    path("hello/<str:name>/", views.hello_name, name="hello_name"),
    path(
        "articles/<int:year>/<int:month>/<slug:slug>/",
        views.article_detail,
        name="article_detail",
    ),
    path(
        "users/<str:username>/articles/",
        views.user_articles,
        name="user_articles",
    ),
    path("advanced-search/", views.advanced_search, name="advanced_search"),
    path("color-filter/", views.color_filter, name="color_filter"),
    path("contact/", views.contact, name="contact"),
    path("cookie-counter/", views.cookie_counter, name="cookie_counter"),
    path("theme/", views.theme_preference, name="theme_preference"),
    path("cart/", views.shopping_cart, name="shopping_cart"),
]

測試

訪問 http://127.0.0.1:8000/practices/cart/

  1. 點擊「加入 蘋果」按鈕數次
  2. 看到購物車顯示「蘋果 x 3」
  3. 加入其他商品
  4. 點擊「移除」可以刪除特定商品
  5. 點擊「清空購物車」可以清空所有商品
  6. 關閉瀏覽器並重新開啟
  7. 再次訪問購物車,資料依然存在(Session 記住了)

Session 成功運作

Session 的資料:

  • ✅ 存在伺服器端,使用者無法修改
  • ✅ 可以儲存複雜的資料結構(字典、列表)
  • ✅ 重新開啟瀏覽器後依然存在
  • ✅ 可以設定過期時間

查看 Session 資料

在資料庫中查看

Django 的 Session 預設存在資料庫的 django_session 表中。

執行以下命令:

uv run manage.py shell

在 Python shell 中:

Python Shell
from django.contrib.sessions.models import Session  # (1)!

# 取得所有 Session
sessions = Session.objects.all()  # (2)!

# 查看第一個 Session
s = sessions.first()  # (3)!
print(s.session_key)  # Session ID
print(s.get_decoded())  # Session 資料(解碼後)
  1. 匯入 Session 模型
  2. 取得所有 Session
  3. 查看第一個 Session 的資料

你會看到類似這樣的輸出:

輸出範例
{'cart': {'蘋果': 3, '香蕉': 1}}

Session 的結構

每個 Session 包含:

  • session_key:Session ID(存在 Cookie 中)
  • session_data:Session 資料(加密後的字串)
  • expire_date:過期時間

Session 的優缺點與使用場景

優點

Session 的優勢

  1. 安全性高:資料存在伺服器,使用者無法修改
  2. 容量更大:沒有 Cookie 的 4KB 限制
  3. 支援複雜資料:可以儲存字典、列表等
  4. 自動管理:Django 自動處理建立、讀取、更新、刪除

缺點

Session 的限制

  1. 需要儲存空間:使用資料庫或快取
  2. 效能開銷:每次請求都要讀取資料庫
  3. 擴展性問題:多伺服器時需要共享 Session 儲存
  4. 依賴 Cookie:Session ID 還是透過 Cookie 傳送

適合使用 Session 的場景

適合的場景

  • 使用者登入狀態:儲存使用者 ID、權限
  • 購物車:暫存購物車內容
  • 多步驟表單:暫存表單資料
  • 臨時資料:需要跨頁面的暫存資料

不適合使用 Session 的場景

不適合的場景

  • 長期儲存:應該存在資料庫
  • 大量資料:影響效能
  • 跨裝置同步:Session 綁定瀏覽器
  • 公開資料:不需要隱藏的資料可以用 GET 參數

比較表

項目 Cookie Session
儲存位置 瀏覽器端 伺服器端
容量限制 約 4KB 取決於伺服器
安全性 低(可被修改) 高(存在伺服器)
效能 好(不佔伺服器資源) 較差(需要讀取資料庫)
生命週期 可設定(天、月、年) 通常較短(分鐘、小時)
跨網域 可設定 domain 不支援
適用資料 簡單、非敏感 複雜、敏感

視覺化比較

graph TD
    A[需要儲存資料] --> B{資料是否敏感?}
    B -->|是| C[使用 Session]
    B -->|否| D{資料大小?}
    D -->|小於 4KB| E{需要長期保存?}
    D -->|大於 4KB| C
    E -->|是| F[使用 Cookie]
    E -->|否| G{需要跨頁面?}
    G -->|是| C
    G -->|否| H[使用 GET/POST 參數]

    style C fill:#90EE90
    style F fill:#87CEEB
    style H fill:#FFD700

實務建議

選擇指南

使用 Cookie:

  • 使用者偏好設定(語言、主題)
  • 追蹤和分析
  • 簡單的狀態記錄

使用 Session:

  • 使用者登入狀態
  • 購物車
  • 多步驟表單
  • 任何敏感或複雜的資料

組合使用:

  • Session 依賴 Cookie 來傳送 Session ID
  • 可以同時使用兩者來達成不同目的

安全性建議

重要的安全設定

在生產環境中,務必設定:

settings.py
# Cookie 安全設定
SESSION_COOKIE_SECURE = True  # 只在 HTTPS 傳送
SESSION_COOKIE_HTTPONLY = True  # 禁止 JavaScript 存取
SESSION_COOKIE_SAMESITE = "Lax"  # 防止 CSRF 攻擊

# Session 設定
SESSION_COOKIE_AGE = 1209600  # 2 週(秒)
SESSION_EXPIRE_AT_BROWSER_CLOSE = False  # 關閉瀏覽器後是否刪除
SESSION_SAVE_EVERY_REQUEST = False  # 每次請求都更新過期時間

這些設定可以大幅提升安全性!

進階:Session 的儲存方式

切換到快取儲存

如果你的網站流量很大,可以使用快取(如 Redis)來儲存 Session,以提升效能。

Django 5.1+ 內建 Redis 支援

從 Django 5.1 開始,Django 內建了 Redis cache backend,不需要額外安裝第三方套件!

設定

編輯 settings.py

settings.py
# Cache 設定
CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",  # (1)!
        "LOCATION": "redis://127.0.0.1:6379/1",  # (2)!
    }
}

# 使用快取作為 Session 儲存
SESSION_ENGINE = "django.contrib.sessions.backends.cache"  # (3)!
SESSION_CACHE_ALIAS = "default"  # (4)!
  1. 使用 Django 內建的 Redis cache backend
  2. Redis 伺服器位置(預設為本機 6379 埠,資料庫 1)
  3. 設定 Session 使用快取儲存
  4. 指定使用哪個快取設定

儲存方式比較

方式 優點 缺點 適用場景
資料庫 可靠、持久化 較慢 一般網站
快取 快速 伺服器重啟後消失 高流量網站
快取+資料庫 快速且可靠 需要額外設定 大型網站

任務結束

完成!

恭喜你完成了這個章節!現在你已經:

  • 了解什麼是 Cookie
  • 學習如何在 Django 中操作 Cookie
  • 認識 Cookie 的優缺點與使用場景
  • 了解什麼是 Session
  • 學習如何在 Django 中操作 Session
  • 認識 Session 的優缺點與使用場景
  • 比較 Cookie 與 Session 的差異

你學會了:

  1. Cookie 操作

    • 讀取:request.COOKIES.get("key")
    • 設定:response.set_cookie("key", value, max_age=...)
    • 實際應用:訪問計數器、主題偏好
  2. Session 操作

    • 讀取:request.session.get("key")
    • 設定:request.session["key"] = value
    • 刪除:del request.session["key"]
    • 實際應用:購物車功能
  3. 選擇原則

    • 簡單、非敏感資料 → Cookie
    • 複雜、敏感資料 → Session
    • 根據需求靈活運用