身分認證——Session 認證與全域設定
文章目錄
2024 iThome 鐵人賽
這是 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:
1 | # user/schemas.py |
然後是 view 函式:
1 | from django.contrib.auth import authenticate, login |
非常簡單!
附帶一提,我不太喜歡程式中有「不必要」的else
,此時的寫法仍不盡理想——因為else
完全可以省略。
在最新的程式碼中,你可以看到我已改成:
1 | user = authenticate(...) |
這樣的做法即所謂的 Guard Clause 或 Early Return(雖然這裡是 raise)。
簡評與重要補充
authenticate
和login
的用法幾乎是固定的,很容易理解:
authenticate
在驗證成功時會 return 對應的User
物件,失敗時則返回None
。login
不會 return,但request
和user
為必要的參數。
成功登入後,你會得到 200 回應,並獲得兩組 cookie:
這對於 API client(比如 Postman)使用者很重要,畢竟瀏覽器會自動幫你存,但這些工具可不會——好吧,我錯了,至少我用的 RapidAPI 會自動存儲、發送!
(我測試 API 時還覺得奇怪,怎麼認證防護都失效了🤣)
如果工具沒有幫你做,記得自己在請求的 headers 加上:
1 | POST /users/2/avatar/ |
authenticate
預設是以AbstractUser
的username
欄位和密碼作為認證基準,如果想用別的欄位,比如email
,則要自己覆寫 Django 的認證後端。
為 API 加上「認證保護」
登入功能完成後,接下來要將「需要登入才能存取」的 API,分別加上認證保護,使用 Django Ninja 提供的django_auth
——這是專門給 Django 內建的 session 認證使用。
我們以「上傳 avatar」API 為例:
1 | from ninja.security import django_auth |
這個例子中,auth=django_auth
確保只有「已登入的使用者」才能存取此 API,否則將得到 401 或 403 回應。
Django Ninja 的 request.auth
但你可能會想到:
光是驗證「已登入」還不夠吧?
「上傳 avatar」應該只能幫「自己」上傳,總不能幫「別人」上傳大頭照吧!
沒錯,所以我們在 view 函式內部,還要多一層驗證。
Django 的request.user
傳統的 Django 專案,我們會透過函式的第一參數——request
,用request.user
來獲得當前使用者資訊,比如:(參考文件)
1 | if request.user.is_authenticated: |
具體來說:
- 當使用者已登入,
request.user
會是一個User
實例,代表當前登入的使用者。 - 未登入時,
request.user
則是一個AnonymousUser
實例,代表未登入使用者。
當使用者已登入,我們可以檢查request.user
的屬性,比如request.user.id
,來確認是否為「本人」。
Django Ninja 的request.auth
但寫 Django Ninja 則需要使用它提供的request.auth
,實作結果如下:
1 | ... |
測試一下,登入後在 URL path 打別人的 id 來呼叫此 API:
1 | // 403 Forbidden |
非常好!
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
中加入下面內容:
1 | # NinjaForum/api.py |
如此一來,全部的 API 都預設擁有認證保護,你可以在特定 API 中排除,比如「登入使用者」:
1 |
在路由裝飾器中,把auth
定義為None
,解除認證保護。
測試認證保護
我們來測試一下「有認證保護」的 API,你會發現在未登入的情況下,嘗試不同 HTTP 方法的 API,你將會得到不同的錯誤回應:
- GET:401 Unauthorized
- POST:403 Forbidden
所以前面才會說你會得到「401 或 403」回應。
測試「取得所有使用者」API
在我們的專案設計中,只有登入的使用者才能存取「取得所有使用者」API。
未登入的情況下,你會得到 401 回應:
1 | // 401 Unauthorized |
測試「新增文章」API
未登入也無法存取「新增文章」API——這顯然非常合理,否則文章不就沒作者了😅
你會得到 403 回應:
1 | // 403 Forbidden |
你心想:「奇怪?為什麼是 CSRF check Failed?」
這是 Django 的 CSRF 保護機制,因為我們的 API 是 POST 方法,所以 Django 會自動檢查 CSRF token,但我們沒有提供 CSRF token,所以就會出現這個錯誤。
小結與下一步
在這篇文章中,我們探討了 Django 的 session 認證與 Django Ninja 的整合,實作了「使用者登入」API,並為其他 API 加上認證保護。最後還示範了如何實現全域認證,讓整個流程更加簡單。
這個系列的最後實踐,我們要來為專案——寫測試!
下一篇將探討,如何使用 test client 和 pytest 來為我們的 Django API 撰寫單元測試。這不僅能幫助我們驗證現有功能,還能為未來的開發和重構提供多一層的保障。