Session 認證#

這是 Django Ninja 系列教學的第 28 篇。
歡迎來到第七章!本章總共有兩篇內容:
- 卷 28:身分認證——Session 認證與全域設定
- 卷 29:單元測試——使用 Test Client 與 pytest 測試 API
這些主題的核心功能,並非由 Django Ninja 實作,但框架仍提供了一定程度的整合。並且,這些功能對於任何 Django 專案來說,都至關重要。
本文介紹幾乎所有 API 專案都需要的——身分認證(Authentication)。
我們將探討如何在 Django Ninja 中利用 Django 內建的 session-based 認證,實現完整的登入驗證功能,並進一步說明如何設定全域認證,以減少程式碼的重複。
本文所有的程式碼改動,可參考這個 PR。
範例專案#
認證的兩個層次#
進入實作前,我們要先了解,所謂的身分認證,究竟代表什麼。
以「帳號密碼 + session 認證」為例,身分認證的範圍主要涵蓋兩個階段。
首先,當使用者透過帳號密碼進行登入時,系統會檢查這些內容、確認身分合法。登入成功後,系統會將使用者資訊(比如用戶 id)儲存至 session,以維持登入狀態。
這是登入時的認證,也是我們最常說的認證。(狹義的認證)
接著,當使用者嘗試存取受「認證保護」的 API 時,系統會檢查 session 並確認身分,確保每個 API 請求都來自合法登入的使用者。
簡言之:
- 第一階段:初次登入時的身分確認。
- 第二階段:後續請求時的身分確認。
兩個層次相輔相成、一體兩面,確保服務能夠在使用者登入和後續操作中,提供適當的安全保障。
實作「使用者登入」API#
了解了上述兩個層次後,我們要先來實作「狹義」的認證——也就是登入驗證本身。
我們將建立一個「使用者登入」API,並直接透過 Django 的 authenticate 和 login 函式處理帳號密碼驗證和登入狀態——非常方便!
authenticate 用來驗證使用者輸入的帳號(username)和密碼是否正確,login 則將使用者的登入狀態儲存至 session。
程式碼實作#
先新增一個登入請求 Schema:
# user/schemas.py
class LoginRequest(Schema):
username: str = Field(examples=['Alice'])
password: str = Field(examples=['password123'])
然後是 view 函式:
from django.contrib.auth import authenticate, login
from user.schemas import CreateUserRequest, LoginRequest
...
@router.post('/users/login/', summary='登入使用者')
def login_user(
request: HttpRequest, payload: LoginRequest
) -> dict[str, str]:
"""
登入使用者
"""
user = authenticate(
request,
username=payload.username,
password=payload.password
)
if user is not None:
login(request, user) # 將使用者登入狀態儲存至 session
return {'message': '登入成功'}
else:
raise HttpError(401, '帳號或密碼錯誤')
非常簡單!
附帶一提,我不太喜歡程式中有「不必要」的 else,此時的寫法仍不盡理想——因為 else 完全可以省略。
在最新的程式碼中,你可以看到我已改成:
user = authenticate(...)
if user is None:
raise HttpError(401, '帳號或密碼錯誤')
login(request, user) # 將使用者登入狀態儲存至 session
return {'message': '登入成功'}
這樣的做法即所謂的 Guard Clause 或 Early Return(雖然這裡是 raise)。
簡評與重要補充#
authenticate 和 login 的用法幾乎是固定的,很容易理解:
authenticate在驗證成功時會 return 對應的User物件,失敗時則返回None。login不會 return,但request和user為必要的參數。
成功登入後,你會得到 200 回應,並獲得兩組 cookie:

這對於 API client(比如 Postman)使用者很重要,畢竟瀏覽器會自動幫你存,但這些工具可不會——好吧,我錯了,至少我用的 RapidAPI 會自動存儲、發送!
(我測試 API 時還覺得奇怪,怎麼認證防護都失效了🤣)
如果工具沒有幫你做,記得自己在請求的 headers 加上:
authenticate 預設是以 AbstractUser 的 username 欄位和密碼作為認證基準,如果想用別的欄位,比如 email,則要自己覆寫 Django 的認證後端。
為 API 加上「認證保護」#
登入功能完成後,接下來要將「需要登入才能存取」的 API,分別加上認證保護,使用 Django Ninja 提供的 django_auth——這是專門給 Django 內建的 session 認證使用。
我們以「上傳 avatar」API 為例:
from ninja.security import django_auth
...
@router.post(
path='/users/{int:user_id}/avatar/',
summary='上傳 avatar',
auth=django_auth # 加上這組參數
)
這個例子中,auth=django_auth 確保只有「已登入的使用者」才能存取此 API,否則將得到 401 或 403 回應。
Django Ninja 的 request.auth#
但你可能會想到:
光是驗證「已登入」還不夠吧?
「上傳 avatar」應該只能幫「自己」上傳,總不能幫「別人」上傳大頭照吧!
沒錯,所以我們在 view 函式內部,還要多一層驗證。
Django 的 request.user#
傳統的 Django 專案,我們會透過函式的第一參數——request,用 request.user 來獲得當前使用者資訊,比如:(參考文件)
if request.user.is_authenticated:
# Do something for authenticated users.
...
else:
# Do something for anonymous users.
...
具體來說:
- 當使用者已登入,request.user 會是一個 User 實例,代表當前登入的使用者。
- 未登入時,request.user 則是一個 AnonymousUser 實例,代表未登入使用者。
當使用者已登入,我們可以檢查 request.user 的屬性,比如 request.user.id,來確認是否為「本人」。
Django Ninja 的 request.auth#
但寫 Django Ninja 則需要使用它提供的 request.auth,實作結果如下:
...
def upload_avatar(...) -> dict[str, str]:
"""
上傳 avatar
"""
# 檢查登入的使用者是否為「本人」
if request.auth.id != user_id:
raise HttpError(403, '無權限上傳其他使用者的 avatar')
...
測試一下,登入後在 URL path 打別人的 id 來呼叫此 API:
非常好!
request.auth 解析#
雖然這裡用 request.auth 來取代 request.user,但其實兩者的內涵有很大的不同。
在 Django Ninja 中,request.auth 代表的是認證流程 return 的結果。此外,Django Ninja 允許你自定義認證方法,所以 request.auth 的內容是不固定的。
讓我們深入了解一下。
認證結果#
request.auth 包含了當前認證方法返回的值。
- 這個值可以是任何類型,取決於你如何實現認證邏輯。
- 這給了開發者極大的靈活性,它可以是 User 物件、字串、Python 字典等等。
認證方法與常見用例#
- 使用 Django 的 session 認證時,
request.auth是 Django 的User物件。 - 對於 API key 認證,
request.auth可能是 API key 本身或與之相關的資訊。 - 在 JWT 認證中,
request.auth可能包含解碼後的 token 資訊。
總之,只要記得,想在 view 函式內進一步取得認證資訊,要透過 request.auth。
這樣就已經實作完認證了,但我們可以讓事情更「簡單」一點。
全域認證的設定與例外#
一一對每個 API 設定認證保護,感覺有點繁瑣——尤其在 API 多的時候。
對此,Django Ninja 支援全域認證,讓所有 API 預設都直接受到保護,開發者只需在特定路由中進行例外處理,排除不想套用的 API 即可。
實作上非常簡單,Django Ninja 直接提供了 SessionAuth 認證類別,用來處理全域的 session-based 認證。
實作全域認證:使用 SessionAuth#
在專案的 api.py 中加入下面內容:
# NinjaForum/api.py
from ninja.security import SessionAuth
...
api = NinjaAPI(
auth=SessionAuth(), # 設定全域認證
...
)
如此一來,全部的 API 都預設擁有認證保護,你可以在特定 API 中排除,比如「登入使用者」:
@router.post(path='/users/login/', summary='登入使用者', auth=None)
在路由裝飾器中,把 auth 定義為 None,解除認證保護。
測試認證保護#
我們來測試一下「有認證保護」的 API,你會發現在未登入的情況下,嘗試不同 HTTP 方法的 API,你將會得到不同的錯誤回應:
- GET:401 Unauthorized
- POST:403 Forbidden
所以前面才會說你會得到「401 或 403」回應。
測試「取得所有使用者」API#
在我們的專案設計中,只有登入的使用者才能存取「取得所有使用者」API。
未登入的情況下,你會得到 401 回應:
測試「新增文章」API#
未登入也無法存取「新增文章」API——這顯然非常合理,否則文章不就沒作者了😅
你會得到 403 回應:
你心想:「奇怪?為什麼是 CSRF check Failed?」
這是 Django 的 CSRF 保護機制,因為我們的 API 是 POST 方法,所以 Django 會自動檢查 CSRF token,但我們沒有提供 CSRF token,所以就會出現這個錯誤。
小結與下一步#
在這篇文章中,我們探討了 Django 的 session 認證與 Django Ninja 的整合,實作了「使用者登入」API,並為其他 API 加上認證保護。最後還示範了如何實現全域認證,讓整個流程更加簡單。
這個系列的最後實踐,我們要來為專案——寫測試!
下一篇將探討,如何使用 test client 和 pytest 來為我們的 Django API 撰寫單元測試。這不僅能幫助我們驗證現有功能,還能為未來的開發和重構提供多一層的保障。