目錄
Django TokenAuthentication
這個名字很容易誤解,這應該是session-based的一種,只是把 SessionAuthentication 的cookie改到http header手動帶上。
這是因為在 mobile app 或是 machine-to-machine 的情況下比較方便,不用依賴瀏覽器「Request 帶上 cookie」的機制。
因為不是 cookie,所以天生對CSRF免疫,但跟SessionAuthentication一樣會怕XSS,只是cookie還能設定http-only去擋XSS,所以最大的敵人可以說是XSS。
Django JWT Authentication 實作
Q: simplejwt 做了什麼?
- 一組現成的
/token//token/refresh/view - 一個
JWTAuthenticationclass 讓 DRF 每個 request 驗Authorization: Bearerheader AccessToken/RefreshToken物件讓你之後 Google OAuth 完成時「自己發 JWT」也用得到
Q: 設定這行背後發生了什麼事情?
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
}
告訴 DRF:每個 APIView 處理 request 時,authentication 階段要使用哪些 class
-
Django 啟動時載入
settings.pyDRF 讀到字串:"rest_framework_simplejwt.authentication.JWTAuthentication"接著動態 import,大致可想成:
from rest_framework_simplejwt.authentication import JWTAuthentication -
每個 DRF View 取得預設設定
APIView本身大致有這種概念:class APIView: authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES所以你寫:
class ProfileView(APIView): ...即使沒有指定
authentication_classes,它仍會繼承全域設定:authentication_classes = [JWTAuthentication] -
Request 進入 DRF 時建立驗證器 DRF 看到這個 View 要使用
JWTAuthentication,所以先建立一個JWTAuthentication物件,之後再叫它檢查 request# 不是啟動時全域只建立一次,而是處理 DRF request 時,`APIView` 都會建立一次,接著把這些物件交給 DRF 的 `Request` authenticators = [ authentication_class() for authentication_class in self.authentication_classes ] request = Request( original_django_request, authenticators=authenticators, )之後當 DRF 要取得:
request.user它就逐一呼叫:
JWTAuthentication().authenticate(request)完整的心智圖:
settings.py REST_FRAMEWORK │ └─ DEFAULT_AUTHENTICATION_CLASSES │ └─ JWTAuthentication class │ HTTP Request │ ↓ │ Django Middleware │ ↓ │ URL Router │ ↓ │ APIView.dispatch() │ ↓ │ 建立 DRF Request │ ↓ │ 建立 JWTAuthentication() ↓ authenticate(request) ↓ 解析 Authorization: Bearer ... ↓ 設定 request.user / request.auth ↓ 執行 permission_classes ↓ 執行 view 的 get() / post()
Q: TokenObtainPairView 底層怎麼驗 username & password
「登入怎麼驗證」直接決定「註冊怎麼建立使用者」
TokenObtainPairView
→ TokenObtainPairSerializer.validate()
→ authenticate(username=..., password=...) ← Django 內建
→ ModelBackend.authenticate()
→ user.check_password(raw_password) ← 關鍵在這
1. 從 DB 撈出這個 user 的 `password` 欄位,是一段 hash。Django 預設存的長這樣:`pbkdf2_sha256$600000$<salt>$<hash>`(演算法$疊代次數$鹽$雜湊值)。
2. 把使用者這次送來的**明文**,用**同樣的演算法 + 同一個 salt** 重算一次 hash。
3. 比對兩個 hash 相不相等。相等 → 密碼正確。
Register endpoint 的重點:
- 覆寫 serializer 的
create()用create_user或是set_password passwordwrite_only,因為不該回傳- view
AllowAny開洞,因為註冊 endpoint 不應該擋權限 - 密碼強度驗證,DRF 的
BaseSerializer類別中有一段邏輯,會掃描物件中所有以validate_<field_name>開頭的方法,要在validate_password裡面呼叫 Django 內建的驗證函式 - 重複 username已經免費被處理了,是因為 Django
User.username欄位本身有unique=True,而ModelSerializer會自動幫這種欄位生一個UniqueValidator
Q: 如何接上 Google Oauth 2.0
Django Server Session
這行程式碼
request.session["oauth_state"] = state效果是:
- 把
oauth_state寫進目前這個使用者的 Django session。 - Django 在 response 結束時保存 session 資料在資料庫中。
- 瀏覽器收到或沿用一個
sessionidcookie。(「寫資料進 DB + 發 sessionid 給你」都是 SessionMiddleware 在回應階段幫你做的) - 下次 callback 時,瀏覽器帶回
sessionid。 - Django 根據
sessionid找回:
request.session["oauth_state"]預設資料庫型 session 下 (存在 django_session),概念上是:
瀏覽器 cookie:
sessionid=abc123
Server session storage:
abc123 → {"oauth_state": "random-state"}所以 state 通常存在 server 的 DB/Redis;cookie 裡主要只是 session ID,不是直接塞完整 state。
Django Server Session 會導致無法擴展,有其他想法嗎?
OAuth 2.0(RFC 6749 §10.12,以及後來的 Security BCP RFC 9700)要求你帶 state 來防 callback 的 CSRF——這是標準。但標準只規定 state 要不可猜測、且能綁回發起這次流程的使用者,沒規定你要存在哪。
核心想法:把「證明這是我發出的」這件事,從「查 server 端記錄」換成「驗簽章」。
-
可以把 HMAC 想成會吃 secret 的 hash,其實 HMAC 不陌生,因為 JWT 就是格式標準化的 payload + HMAC 簽章
signature = HMAC(secret, message)HMAC 不是加密
HMAC 提供: ✓ 完整性 integrity ✓ 真實性 authenticity HMAC 不提供: ✗ 機密性 confidentiality -
nonce:一次性的隨機識別碼,用來表示「這一次請求」
流程:
發起時(login):
- 組一個 payload:一段隨機 nonce +(通常)一個過期時間戳。
payload = { "nonce": "x4ToYF3n7K8q", "exp": 1784523600 } - 用 server 的 secret 對它簽章(HMAC,或直接做成短效期 JWT),
state = payload + 簽章。signature = HMAC(server_secret, payload) state = base64(payload) + "." + base64(signature) - 塞進 redirect URL 給 Google, server 什麼都不存。
回來時(callback):
- 拿到
state,用同一把 secret 驗簽章 + 檢查沒過期。state = payload.signature 1. 拆出 payload 2. 使用相同 secret 重新計算 HMAC 3. 比較兩個 signature 4. 檢查 exp 是否過期 - 簽章對 → 這一定是我剛才簽發的、還沒過期 → 信任。
漏洞:可以信任這份 payload 確實由 server 簽發且沒有被修改,但不一定能證明是目前這個瀏覽器的這一次登入流程。
要證明是同一個瀏覽器,會透過 double-submit,也就是綁定瀏覽器 Cookie。
但還是無法擋掉 replay attack,但在 OAuth 裡要注意:authorization code 通常本身就是一次性的,第一次兌換後,第二次通常會失敗。所以單純重播同一組 code + state,威脅常常有限。