使用表單傳遞 POST 參數¶
開始之前¶
任務目標
在這個章節中,我們會完成:
- 了解 HTTP 方法(GET、POST 等)
- 比較 GET 與 POST 的差異和使用時機
- 使用表單傳遞 POST 參數
- 了解 CSRF 攻擊與防護機制
HTTP 方法¶
HTTP 定義了多種請求方法,用來表示對資源的不同操作:
| 方法 | 說明 | 用途 |
|---|---|---|
GET |
取得資源 | 讀取資料、搜尋、篩選 |
POST |
建立資源 | 提交表單、新增資料 |
PUT |
替換資源 | 完整替換 (replace) 現有資源 |
PATCH |
更新資源 | 修改部分欄位 |
DELETE |
刪除資源 | 刪除資料 |
RESTful API
在 RESTful API 設計中,這些方法有明確的語意。但在傳統的網頁表單中,主要使用 GET 和 POST。
更多關於 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:
<!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¶
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¶
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/,填寫表單後送出。
注意觀察:
- 表單送出後,網址不會改變(不像 GET 會加上參數)
- 頁面會顯示你填寫的內容
- 如果忘記加
{% 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,讓伺服器知道「這是同一個使用者」。
更多資訊請參考 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)
防護原理¶
- Django 生成 Token:當使用者訪問含有表單的頁面時,Django 會生成一個隨機的 CSRF Token
- Token 存放兩處:
- Cookie 中(瀏覽器會自動發送)
- 表單的隱藏欄位中(需要在頁面上才能取得)
- 驗證 Token:提交表單時,Django 會比對 Cookie 中的 Token 和表單中的 Token 是否一致
- 惡意網站無法偽造:惡意網站無法讀取 Django 網站的頁面內容,所以無法取得正確的 Token
{% csrf_token %} 會在表單中插入一個隱藏欄位:
這個值是隨機生成的,每次都不同,惡意網站無法預測。
SameSite Cookie 不是也能防護 CSRF 嗎?
是的!現代瀏覽器支援 Cookie 的 SameSite 屬性,可以限制 Cookie 只在「同站請求」時發送:
| SameSite 值 | 說明 |
|---|---|
Strict |
只有同站請求才會發送 Cookie |
Lax |
同站請求 + 從外站導航的 GET 請求會發送 Cookie |
None |
所有請求都會發送(需搭配 Secure) |
當 SameSite=Strict 或 Lax 時,惡意網站發送的跨站 POST 請求不會附帶 Cookie,CSRF 攻擊就會失敗。
那為什麼還需要 CSRF Token?
- 瀏覽器相容性:舊版瀏覽器不支援 SameSite
- Lax 模式的限制:Lax 允許 GET 請求附帶 Cookie,如果網站錯誤地用 GET 處理敏感操作,仍有風險
- 子網域攻擊:SameSite 無法防護來自子網域的攻擊(如
evil.example.com攻擊www.example.com) - 防禦深度:安全最佳實踐是多層防護,不依賴單一機制
Django 預設使用 SameSite=Lax,但仍然要求 CSRF Token,這是防禦深度(Defense in Depth)的體現。
為什麼需要 Cookie 和表單兩個 Token?
你可能會疑惑:為什麼 Django 要在 Cookie 和表單中都放 Token,而不是只用一個?
關鍵在於「誰能存取什麼」:
| 項目 | 惡意網站能做到嗎? |
|---|---|
| 讓瀏覽器發送請求到目標網站 | ✓ 可以 |
| 讓瀏覽器自動附帶目標網站的 Cookie | ✓ 可以(瀏覽器自動行為) |
| 讀取目標網站的 Cookie 內容 | ✗ 不行(同源政策限制) |
| 讀取目標網站的頁面內容 | ✗ 不行(同源政策限制) |
單一 Token 的問題:
- 只放 Cookie:惡意網站雖然無法讀取 Cookie,但瀏覽器會自動發送,所以無法驗證請求來源
- 只放表單:沒有一個「已知的正確值」可以比對
雙 Token 驗證的原理:
- Cookie 中的 Token:作為「伺服器認可的正確值」
- 表單中的 Token:證明「請求來自真正的頁面」
惡意網站可以觸發請求並讓瀏覽器附帶 Cookie,但它無法讀取頁面內容來取得表單中的 Token。當伺服器比對兩個 Token 時,就能發現表單中的 Token 不正確(或不存在),從而拒絕請求。
這種方式稱為 Double Submit Cookie 或 Synchronizer Token Pattern。
忘記 csrf_token 會發生什麼?¶
如果你在 POST 表單中忘記加入 {% csrf_token %}:
Django 會拒絕這個請求,因為無法驗證請求的來源。
永遠不要關閉 CSRF 防護
雖然可以透過設定關閉 CSRF 防護,但這會讓你的網站暴露在 CSRF 攻擊的風險中。
請永遠在 POST 表單中加入 {% csrf_token %}。
任務結束¶
完成!
恭喜你完成了這個章節!現在你已經:
- 了解 HTTP 方法(GET、POST 等)
- 比較 GET 與 POST 的差異和使用時機
- 使用表單傳遞 POST 參數
- 了解 CSRF 攻擊與防護機制