跳轉到

使用表單傳遞 POST 參數

開始之前

任務目標

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

  • 了解 HTTP 方法(GET、POST 等)
  • 比較 GET 與 POST 的差異和使用時機
  • 使用表單傳遞 POST 參數
  • 了解 CSRF 攻擊與防護機制

HTTP 方法

HTTP 定義了多種請求方法,用來表示對資源的不同操作:

方法 說明 用途
GET 取得資源 讀取資料、搜尋、篩選
POST 建立資源 提交表單、新增資料
PUT 替換資源 完整替換 (replace) 現有資源
PATCH 更新資源 修改部分欄位
DELETE 刪除資源 刪除資料

RESTful API

在 RESTful API 設計中,這些方法有明確的語意。但在傳統的網頁表單中,主要使用 GETPOST

更多關於 HTTP 方法的資訊,請參考 MDN - HTTP 請求方法

GET 參數 vs POST 參數

GET

  • 參數放在 URL 中(?key=value
  • 有長度限制(約 2048 字元)
  • 會被瀏覽器快取
  • 會留在瀏覽器歷史記錄
  • 可以被加入書籤
  • 不適合傳送敏感資料

POST

  • 參數放在請求主體(Request Body)中
  • 沒有長度限制
  • 不會被瀏覽器快取
  • 不會留在瀏覽器歷史記錄
  • 不能被加入書籤
  • 適合傳送敏感資料

選擇指南

什麼時候用 GET?

  • 搜尋、篩選、排序
  • 分頁
  • 讀取資料
  • 需要分享連結的操作

什麼時候用 POST?

  • 登入、註冊
  • 新增、修改、刪除資料
  • 上傳檔案
  • 傳送敏感資訊(密碼、信用卡等)

比較表

特性 GET POST
參數位置 URL Request Body
可見性 網址列可見 不可見
書籤 可以 不可以
快取 不會
長度限制
安全性 較低 較高
冪等性 是(重複請求結果相同)

建立 POST 表單

步驟 1:建立 Template

建立 practices/templates/practices/contact.html

practices/templates/practices/contact.html
<!DOCTYPE html>
<html lang="en">
  <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>Contact</title>
  </head>
  <body>
    <h1>
      聯絡我們
    </h1>

    <form method="post" action="">
      {% csrf_token %}
      <div>
        <label for="name">
          姓名:
        </label>
        <input type="text" id="name" name="name" required />
      </div>
      <div>
        <label for="email">
          Email:
        </label>
        <input type="email" id="email" name="email" required />
      </div>
      <div>
        <label for="message">
          訊息:
        </label>
        <textarea id="message" name="message" rows="5" required></textarea>
      </div>
      <button type="submit">
        送出
      </button>
    </form>

    {% if success %}
      <div>
        <h2>
          感謝您的訊息!
        </h2>
        <p>
          姓名: {{ name }}
        </p>
        <p>
          Email: {{ email }}
        </p>
        <p>
          訊息: {{ message }}
        </p>
      </div>
    {% endif %}
  </body>
</html>

重要:csrf_token

注意表單中的 {% csrf_token %},這是 Django 的安全機制。

所有 POST 表單都必須加上這個標籤,否則會收到 403 Forbidden 錯誤。我們稍後會詳細說明。

步驟 2:建立 View

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)

request.method

request.method 可以判斷目前的請求方法:

  • "GET" - 使用者訪問頁面
  • "POST" - 使用者提交表單

request.POST

request.POST 是一個類似字典的物件,包含所有 POST 參數。

使用方式和 request.GET 相同:

  • request.POST.get("key") - 取得單一值
  • request.POST.get("key", default) - 取得單一值,不存在時回傳預設值
  • request.POST.getlist("key") - 取得多個值

步驟 3:設定 URL

practices/urls.py
from django.urls import path

from practices import views

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"),
]

步驟 4:測試

訪問 http://127.0.0.1:8000/practices/contact/,填寫表單後送出。

注意觀察:

  1. 表單送出後,網址不會改變(不像 GET 會加上參數)
  2. 頁面會顯示你填寫的內容
  3. 如果忘記加 {% csrf_token %},會看到 403 Forbidden 錯誤

Email 的驗證

現在如果你在 Email 欄位輸入非電子郵件,你會發現無法送出。

但這只是瀏覽器的「前端」驗證,我們並沒有在後端驗證資料這樣是不對的,永遠要記得資料需要再前端與後端都逕行驗證。

什麼是 CSRF?

你可能注意到表單中有一個 {% csrf_token %},這是什麼?

CSRF 攻擊原理

CSRF(Cross-Site Request Forgery,跨站請求偽造)是一種網路攻擊方式。讓我們用一個情境來說明:

假設你登入了銀行網站,瀏覽器會保存你的登入狀態(Cookie)。如果你在沒有登出的情況下訪問了一個惡意網站,這個網站可以偷偷發送請求到銀行,而銀行會以為是你本人的操作。

sequenceDiagram
    participant 使用者
    participant 瀏覽器
    participant 銀行網站
    participant 惡意網站

    使用者->>瀏覽器: 1. 登入銀行網站
    瀏覽器->>銀行網站: 登入請求
    銀行網站->>瀏覽器: 登入成功,設定 Cookie

    Note over 瀏覽器: 瀏覽器保存了銀行的 Cookie

    使用者->>瀏覽器: 2. 訪問惡意網站
    瀏覽器->>惡意網站: 請求頁面
    惡意網站->>瀏覽器: 回傳含有隱藏表單的頁面

    Note over 惡意網站,瀏覽器: 惡意網站的頁面包含:<br/>自動提交的轉帳表單

    瀏覽器->>銀行網站: 3. 自動發送轉帳請求<br/>(附帶銀行的 Cookie)

    Note over 銀行網站: 銀行看到有效的 Cookie<br/>以為是使用者的操作

    銀行網站->>銀行網站: 4. 執行轉帳!

為什麼會成功?

關鍵在於瀏覽器的 Cookie 機制

  • 當你登入銀行網站後,瀏覽器會自動保存銀行的 Cookie
  • 之後任何發送到銀行網站的請求,瀏覽器都會自動附帶這個 Cookie
  • 銀行網站只能看到「這是一個帶有有效 Cookie 的請求」,無法分辨是你主動發送的,還是惡意網站偷偷發送的

什麼是 Cookie?

Cookie 是網站儲存在使用者瀏覽器中的小型文字資料,主要用途包括:

  • Session 管理:保持登入狀態、購物車內容
  • 個人化設定:語言偏好、主題設定
  • 追蹤分析:記錄使用者行為

當你登入網站時,伺服器會發送一個包含 Session ID 的 Cookie 給瀏覽器。之後每次請求,瀏覽器都會自動附帶這個 Cookie,讓伺服器知道「這是同一個使用者」。

# Cookie 的格式
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax

更多資訊請參考 MDN - HTTP Cookie

Django 的 CSRF 防護

Django 使用 CSRF Token 來解決這個問題:

sequenceDiagram
    participant 使用者
    participant 瀏覽器
    participant Django 網站
    participant 惡意網站

    使用者->>瀏覽器: 1. 訪問表單頁面
    瀏覽器->>Django 網站: 請求頁面
    Django 網站->>瀏覽器: 回傳頁面<br/>(包含隨機生成的 CSRF Token)

    Note over 瀏覽器: Token 存在於:<br/>1. Cookie 中<br/>2. 表單的隱藏欄位中

    使用者->>瀏覽器: 2. 填寫並提交表單
    瀏覽器->>Django 網站: POST 請求<br/>(Cookie 中的 Token + 表單中的 Token)

    Note over Django 網站: 比對兩個 Token 是否合法

    Django 網站->>瀏覽器: ✓ Token 合法,請求成功

    使用者->>瀏覽器: 3. 訪問惡意網站
    瀏覽器->>惡意網站: 請求頁面
    惡意網站->>瀏覽器: 回傳惡意表單

    Note over 惡意網站: 惡意網站無法得知<br/>Django 網站的 CSRF Token

    瀏覽器->>Django 網站: 4. 發送請求<br/>(只有 Cookie,沒有正確的 Token)
    Django 網站->>瀏覽器: ✗ Token 不一致,拒絕請求 (403)

防護原理

  1. Django 生成 Token:當使用者訪問含有表單的頁面時,Django 會生成一個隨機的 CSRF Token
  2. Token 存放兩處
    • Cookie 中(瀏覽器會自動發送)
    • 表單的隱藏欄位中(需要在頁面上才能取得)
  3. 驗證 Token:提交表單時,Django 會比對 Cookie 中的 Token 和表單中的 Token 是否一致
  4. 惡意網站無法偽造:惡意網站無法讀取 Django 網站的頁面內容,所以無法取得正確的 Token

{% csrf_token %} 會在表單中插入一個隱藏欄位:

<input type="hidden" name="csrfmiddlewaretoken" value="abc123...">

這個值是隨機生成的,每次都不同,惡意網站無法預測。

SameSite Cookie 不是也能防護 CSRF 嗎?

是的!現代瀏覽器支援 Cookie 的 SameSite 屬性,可以限制 Cookie 只在「同站請求」時發送:

SameSite 值 說明
Strict 只有同站請求才會發送 Cookie
Lax 同站請求 + 從外站導航的 GET 請求會發送 Cookie
None 所有請求都會發送(需搭配 Secure

SameSite=StrictLax 時,惡意網站發送的跨站 POST 請求不會附帶 Cookie,CSRF 攻擊就會失敗。

那為什麼還需要 CSRF Token?

  1. 瀏覽器相容性:舊版瀏覽器不支援 SameSite
  2. Lax 模式的限制:Lax 允許 GET 請求附帶 Cookie,如果網站錯誤地用 GET 處理敏感操作,仍有風險
  3. 子網域攻擊:SameSite 無法防護來自子網域的攻擊(如 evil.example.com 攻擊 www.example.com
  4. 防禦深度:安全最佳實踐是多層防護,不依賴單一機制

Django 預設使用 SameSite=Lax,但仍然要求 CSRF Token,這是防禦深度(Defense in Depth)的體現。

為什麼需要 Cookie 和表單兩個 Token?

你可能會疑惑:為什麼 Django 要在 Cookie 和表單中都放 Token,而不是只用一個?

關鍵在於「誰能存取什麼」:

項目 惡意網站能做到嗎?
讓瀏覽器發送請求到目標網站 ✓ 可以
讓瀏覽器自動附帶目標網站的 Cookie ✓ 可以(瀏覽器自動行為)
讀取目標網站的 Cookie 內容 ✗ 不行(同源政策限制)
讀取目標網站的頁面內容 ✗ 不行(同源政策限制)

單一 Token 的問題:

  • 只放 Cookie:惡意網站雖然無法讀取 Cookie,但瀏覽器會自動發送,所以無法驗證請求來源
  • 只放表單:沒有一個「已知的正確值」可以比對

雙 Token 驗證的原理:

  1. Cookie 中的 Token:作為「伺服器認可的正確值」
  2. 表單中的 Token:證明「請求來自真正的頁面」

惡意網站可以觸發請求並讓瀏覽器附帶 Cookie,但它無法讀取頁面內容來取得表單中的 Token。當伺服器比對兩個 Token 時,就能發現表單中的 Token 不正確(或不存在),從而拒絕請求。

這種方式稱為 Double Submit CookieSynchronizer Token Pattern

忘記 csrf_token 會發生什麼?

如果你在 POST 表單中忘記加入 {% csrf_token %}

Forbidden (403)
CSRF verification failed. Request aborted.

Django 會拒絕這個請求,因為無法驗證請求的來源。

永遠不要關閉 CSRF 防護

雖然可以透過設定關閉 CSRF 防護,但這會讓你的網站暴露在 CSRF 攻擊的風險中。

請永遠在 POST 表單中加入 {% csrf_token %}

任務結束

完成!

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

  • 了解 HTTP 方法(GET、POST 等)
  • 比較 GET 與 POST 的差異和使用時機
  • 使用表單傳遞 POST 參數
  • 了解 CSRF 攻擊與防護機制