[Day 13] 登入驗證 (二):JWT
今天來繼續聊登入驗證~
昨天我們介紹了 Basic Auth,今天接著介紹 JWT
什麼是 JWT?
JWT 的全名是 JSON Web Token,與 Basic Auth 類似,一樣也是夾帶了帳號與密碼 (以及其他更多的資訊)。但不同的是,建立 JWT 時需要一把金鑰 (請好好保管 XD),一旦 JWT 被修改過,就會被擁有金鑰的單位發現。
換句話說,JWT 是可以被驗證有效性的
JWT 是由三個部份組合而成的,中間用一個 .
來隔開。這三個部份分別是:
- header
- payload
- signature
header 主要是宣告這個 JWT 使用的演算法,這對後續驗證 JWT 來說是必要資訊。payload 則是放一些方便後續使用的資訊 (例如:帳號、使用者權限),通常也會放這個 JWT 的到期時間。signature 則是驗證 JWT 有效性的關鍵,它會把上面那兩個部份結合在一起後進行加密,因此,一旦無法用金鑰解密,或是資訊與上方不吻合都會被視為 JWT 無效。
JWT 也有不加密的,但這邊就不多討論了
大家可以去看看這篇文章,我覺得介紹得很好,也包含了很多理論的部份
另外,請大家一定要去 JWT.IO 這個網站動手玩玩看,相信對理解 JWT 會有幫助的。它也有整理大量的 JWT 套件 (常見語言都有),並且列出各個套件支援的功能與演算法。
JWT 實作
套件安裝
JWT.IO 上 python 的套件有四個,這邊我們使用的是 python-jose
,與 FastAPI 官網範例使用相同的套件。
需要注意的是,安裝 python-jose
時需要額外安裝加密用的套件,詳情可以看Github的說明。加密套件的選擇有不只一個,官方推薦的是 pyca/cryptography,因此安裝 python-jose
的指令要使用
1 | python-jose[cryptography] |
但我自己是使用 pycryptodome,也沒有遇到什麼問題。
會選這個的原因是,有其他功能需要用到加解密,而那部份已經在使用pycryptodome
了
建立 JWT
接下來我們來簡單的實作 JWT。這邊我使用的金鑰是直接複製 FastAPI 範例的,payload 內容則是使用 JWT.IO 的範例的預設值。
1 | from jose import jwt |
執行後就會在 terminal 看到 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.HHg-h7KYt9hAKhSYmMPgFNu__j78RzWm-t4PfGuCWE4
,這就是我們產生出來的 JWT
另外,我們也可以把這個金鑰貼到 JWT.IO 網站範例右下的 VERIFY SIGNATURE 欄位,取代原本的 your-256-bit-secret
,確認使用的演算法是 HS256
(預設) 之後,就會發現左邊的 JWT 與我們剛剛在 terminal 看到的 JWT 是相同的。
將 JWT 與 FastAPI 整合
大家可以去參考 FastAPI 官網的範例,程式碼太長我就不貼了,我挑幾個部份來解釋一下。
1 | def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): |
這部份就是負責產生 JWT 的函數,與上方範例不同的是,它多了到期時間的設定,因此會把當前時間加上有效時間的結果放進 payload 裡,也就是 to_encode
這個 dictionary
1 | async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): |
這部份則是驗證 JWT,使用的是 jwt.decode()
這個函數,如果
- 解密失敗 (發生
JWTError
) - payload 內沒有
username
這個資訊 - 取得的 username 並不存在於資料庫內
都會 raiseHTTPException
,讓前端拿到 401 錯誤
1 |
|
這個 API 比較像是一般的登入 API,負責回傳建立好的 JWT 給前端。
後端使用了 OAuth2PasswordRequestForm
,因此前端必須要用 username
和 password
把帳號密碼送到後端。後端拿到之後,會用 authenticate_user
函數 (沒貼程式碼) 驗證使用者,而驗證的標準有兩個:
- username 是否存在於資料庫內
- 密碼經過雜湊 (hash) 計算後是否與資料庫相同
如果有任何一個不符合,那authenticate_user
就會得到False
,導致前端拿到 401 錯誤。
若 authenticate_user
為 True
,就會開始製作 JWT,並回傳 JWT 給前端。
這邊寫「雜湊計算」也是相對簡單的寫法,精確一點應該是使用 bcrypt,但這邊就不多作介紹了
1 |
|
這個就是一個需要 JWT 驗證的 API,在函數 input 內包含了 Depends()
,而由於 Depends()
內的函數會先被執行,因此如果在確認後發現沒有有效的 JWT,就會直接回傳 401 錯誤,不會進到 return current_user
這行程式碼。反之,如果 JWT 是有效的,那麼資料庫中的使用者資料就會被帶入這個 API 函數中,接著被回傳給前端。需要注意的是,因為有設定 response_model
,所以有部份資料被過濾掉 (例如:密碼),不會全部都送到前端去。
重點回顧
今天我們介紹了
- 什麼是 JWT?
- JWT 的實作
然而,這部份其實還有很多東西可以討論,例如
- access token & refresh token
- 如何強制登出?
- scope (這個 JWT 的權限範圍)
但這討論下去就跟 FastAPI 沒有太大的關係了,因此身分驗證的主題就先告一個段落了,明天會開始介紹資料庫~
今天來繼續聊登入驗證~
昨天我們介紹了 Basic Auth,今天接著介紹 JWT
什麼是 JWT?
JWT 的全名是 JSON Web Token,與 Basic Auth 類似,一樣也是夾帶了帳號與密碼 (以及其他更多的資訊)。但不同的是,建立 JWT 時需要一把金鑰 (請好好保管 XD),一旦 JWT 被修改過,就會被擁有金鑰的單位發現。
換句話說,JWT 是可以被驗證有效性的
JWT 是由三個部份組合而成的,中間用一個 .
來隔開。這三個部份分別是:
- header
- payload
- signature
header 主要是宣告這個 JWT 使用的演算法,這對後續驗證 JWT 來說是必要資訊。payload 則是放一些方便後續使用的資訊 (例如:帳號、使用者權限),通常也會放這個 JWT 的到期時間。signature 則是驗證 JWT 有效性的關鍵,它會把上面那兩個部份結合在一起後進行加密,因此,一旦無法用金鑰解密,或是資訊與上方不吻合都會被視為 JWT 無效。
JWT 也有不加密的,但這邊就不多討論了
大家可以去看看這篇文章,我覺得介紹得很好,也包含了很多理論的部份
另外,請大家一定要去 JWT.IO 這個網站動手玩玩看,相信對理解 JWT 會有幫助的。它也有整理大量的 JWT 套件 (常見語言都有),並且列出各個套件支援的功能與演算法。
JWT 實作
套件安裝
JWT.IO 上 python 的套件有四個,這邊我們使用的是 python-jose
,與 FastAPI 官網範例使用相同的套件。
需要注意的是,安裝 python-jose
時需要額外安裝加密用的套件,詳情可以看Github的說明。加密套件的選擇有不只一個,官方推薦的是 pyca/cryptography,因此安裝 python-jose
的指令要使用
1 | python-jose[cryptography] |
但我自己是使用 pycryptodome,也沒有遇到什麼問題。
會選這個的原因是,有其他功能需要用到加解密,而那部份已經在使用pycryptodome
了
建立 JWT
接下來我們來簡單的實作 JWT。這邊我使用的金鑰是直接複製 FastAPI 範例的,payload 內容則是使用 JWT.IO 的範例的預設值。
1 | from jose import jwt |
執行後就會在 terminal 看到 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.HHg-h7KYt9hAKhSYmMPgFNu__j78RzWm-t4PfGuCWE4
,這就是我們產生出來的 JWT
另外,我們也可以把這個金鑰貼到 JWT.IO 網站範例右下的 VERIFY SIGNATURE 欄位,取代原本的 your-256-bit-secret
,確認使用的演算法是 HS256
(預設) 之後,就會發現左邊的 JWT 與我們剛剛在 terminal 看到的 JWT 是相同的。
將 JWT 與 FastAPI 整合
大家可以去參考 FastAPI 官網的範例,程式碼太長我就不貼了,我挑幾個部份來解釋一下。
1 | def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): |
這部份就是負責產生 JWT 的函數,與上方範例不同的是,它多了到期時間的設定,因此會把當前時間加上有效時間的結果放進 payload 裡,也就是 to_encode
這個 dictionary
1 | async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): |
這部份則是驗證 JWT,使用的是 jwt.decode()
這個函數,如果
- 解密失敗 (發生
JWTError
) - payload 內沒有
username
這個資訊 - 取得的 username 並不存在於資料庫內
都會 raiseHTTPException
,讓前端拿到 401 錯誤
1 |
|
這個 API 比較像是一般的登入 API,負責回傳建立好的 JWT 給前端。
後端使用了 OAuth2PasswordRequestForm
,因此前端必須要用 username
和 password
把帳號密碼送到後端。後端拿到之後,會用 authenticate_user
函數 (沒貼程式碼) 驗證使用者,而驗證的標準有兩個:
- username 是否存在於資料庫內
- 密碼經過雜湊 (hash) 計算後是否與資料庫相同
如果有任何一個不符合,那authenticate_user
就會得到False
,導致前端拿到 401 錯誤。
若 authenticate_user
為 True
,就會開始製作 JWT,並回傳 JWT 給前端。
這邊寫「雜湊計算」也是相對簡單的寫法,精確一點應該是使用 bcrypt,但這邊就不多作介紹了
1 |
|
這個就是一個需要 JWT 驗證的 API,在函數 input 內包含了 Depends()
,而由於 Depends()
內的函數會先被執行,因此如果在確認後發現沒有有效的 JWT,就會直接回傳 401 錯誤,不會進到 return current_user
這行程式碼。反之,如果 JWT 是有效的,那麼資料庫中的使用者資料就會被帶入這個 API 函數中,接著被回傳給前端。需要注意的是,因為有設定 response_model
,所以有部份資料被過濾掉 (例如:密碼),不會全部都送到前端去。
重點回顧
今天我們介紹了
- 什麼是 JWT?
- JWT 的實作
然而,這部份其實還有很多東西可以討論,例如
- access token & refresh token
- 如何強制登出?
- scope (這個 JWT 的權限範圍)
但這討論下去就跟 FastAPI 沒有太大的關係了,因此身分驗證的主題就先告一個段落了,明天會開始介紹資料庫~