You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

9.1 KiB

簡易 OAuth2:Password 與 Bearer

現在從上一章延伸,補上缺少的部分,完成整個安全流程。

取得 usernamepassword

我們要使用 FastAPI 提供的安全性工具來取得 usernamepassword

OAuth2 規範中,當使用「password flow」(我們現在使用的)時,用戶端/使用者必須以表單資料送出 usernamepassword 欄位。

而且規範要求欄位名稱必須就是這兩個,所以像是 user-nameemail 都不行。

但別擔心,你在前端要怎麼呈現給最終使用者都可以。

而你的資料庫模型也可以使用任何你想要的欄位名稱。

不過在登入的路徑操作(path operation)裡,我們需要使用這些名稱,才能符合規範(例如才能使用整合的 API 文件系統)。

規範也說明 usernamepassword 必須以表單資料傳送(也就是這裡不能用 JSON)。

scope

規範也說用戶端可以再送一個表單欄位「scope」。

欄位名稱是單數的 scope,但實際上是由多個以空白分隔的「scopes」組成的一長串字串。

每個「scope」就是一個(不含空白的)字串。

它們通常用來宣告特定的權限,例如:

  • users:readusers:write 是常見的例子
  • instagram_basic 用在 Facebook / Instagram
  • https://www.googleapis.com/auth/drive 用在 Google

/// note

在 OAuth2 裡,「scope」只是用來宣告特定所需權限的一個字串。

不論裡面是否包含像 : 之類的字元,或是否是一個 URL,都沒差。

那些都是實作細節。

對 OAuth2 而言,它們就是字串而已。

///

取得 usernamepassword 的程式碼

現在用 FastAPI 提供的工具來處理。

OAuth2PasswordRequestForm

先匯入 OAuth2PasswordRequestForm,並在 /token 的路徑操作中,搭配 Depends 當作依賴使用:

{* ../../docs_src/security/tutorial003_an_py310.py hl[4,78] *}

OAuth2PasswordRequestForm 是一個類別型依賴,它宣告了一個表單本文,包含:

  • username
  • password
  • 可選的 scope 欄位,內容是一個由空白分隔的長字串
  • 可選的 grant_type

/// tip

依規範,實際上需要一個 grant_type 欄位且固定值為 password,但 OAuth2PasswordRequestForm 並不會強制檢查。

如果你需要強制檢查,請改用 OAuth2PasswordRequestFormStrict 取代 OAuth2PasswordRequestForm

///

  • 可選的 client_id(本例不需要)
  • 可選的 client_secret(本例不需要)

/// note

OAuth2PasswordRequestForm 並不是像 OAuth2PasswordBearer 那樣對 FastAPI 來說的特殊類別。

OAuth2PasswordBearer 會讓 FastAPI 知道它是一個 security scheme,因此會以那種方式加入 OpenAPI。

OAuth2PasswordRequestForm 只是你也可以自己撰寫的一個類別型依賴,或是你也可以直接宣告 Form 參數。

只是因為這是很常見的用例,所以 FastAPI 直接內建提供,讓事情更簡單。

///

使用表單資料

/// tip

OAuth2PasswordRequestForm 這個依賴類別的實例不會有以空白分隔長字串的 scope 屬性,而是會有一個 scopes 屬性,裡面是各個 scope 的實際字串清單。

本示例沒有使用 scopes,但如果你需要,功能已經在那裡了。

///

現在,從(假的)資料庫裡用表單欄位的 username 取得使用者資料。

如果沒有該使用者,就回傳「Incorrect username or password」的錯誤。

我們用 HTTPException 這個例外來回傳錯誤:

{* ../../docs_src/security/tutorial003_an_py310.py hl[3,79:81] *}

檢查密碼

這時我們已經有來自資料庫的使用者資料,但還沒檢查密碼。

先把那些資料放進 Pydantic 的 UserInDB 模型。

你絕對不要以純文字儲存密碼,所以我們會使用(假的)密碼雜湊系統。

如果密碼不匹配,我們回傳同樣的錯誤。

密碼雜湊(hashing)

「雜湊」的意思是:把一些內容(這裡是密碼)轉換成一串看起來像亂碼的位元組序列(就是字串)。

只要你輸入完全相同的內容(完全相同的密碼),就會得到完全相同的亂碼。

但你無法從這串亂碼還原回原本的密碼。

為何要做密碼雜湊

如果你的資料庫被竊取,攻擊者拿到的不是使用者的純文字密碼,而只是雜湊值。

因此攻擊者無法嘗試把那些密碼用在其他系統上(因為很多使用者在各處都用同一組密碼,這會很危險)。

{* ../../docs_src/security/tutorial003_an_py310.py hl[82:85] *}

關於 **user_dict

UserInDB(**user_dict) 的意思是:

user_dict 的鍵和值直接當作具名參數傳入,等同於:

UserInDB(
    username = user_dict["username"],
    email = user_dict["email"],
    full_name = user_dict["full_name"],
    disabled = user_dict["disabled"],
    hashed_password = user_dict["hashed_password"],
)

/// note

想更完整地了解 **user_dict,請回到額外模型 的文件

///

回傳 token

token 端點的回應必須是 JSON 物件。

它應該有一個 token_type。在本例中,我們使用「Bearer」tokens,token 類型應該是「bearer」。

而且它還應該有一個 access_token,其值為包含我們存取權杖的字串。

在這個簡單示例中,我們會不安全地直接回傳相同的 username 當作 token。

/// tip

下一章你會看到真正安全的實作,包含密碼雜湊與 JWT tokens。

但現在先把注意力放在我們需要的這些細節上。

///

{* ../../docs_src/security/tutorial003_an_py310.py hl[87] *}

/// tip

依照規範,你應該回傳一個包含 access_tokentoken_type 的 JSON,就像這個範例。

這部分需要你自己在程式中完成,並確保使用這些 JSON key。

這幾乎是你為了符合規範而必須自行記得正確處理的唯一事情。

其餘的 FastAPI 都會幫你處理。

///

更新依賴項

接著我們要更新依賴項。

我們只想在使用者為啟用狀態時取得 current_user

所以,我們新增一個依賴 get_current_active_user,而它本身又依賴 get_current_user

這兩個依賴會在使用者不存在或未啟用時回傳 HTTP 錯誤。

因此,在端點中,只有在使用者存在、已正確驗證且為啟用狀態時,我們才會取得使用者:

{* ../../docs_src/security/tutorial003_an_py310.py hl[58:66,69:74,94] *}

/// note

這裡我們一併回傳值為 Bearer 的額外標頭 WWW-Authenticate,這也是規範的一部分。

任何 HTTP(錯誤)狀態碼 401「UNAUTHORIZED」都應該同時回傳 WWW-Authenticate 標頭。

在 bearer tokens(我們的情況)下,該標頭的值應該是 Bearer

其實你可以省略這個額外標頭,功能仍會正常。

但此處加上它是為了遵循規範。

同時也可能有工具會期待並使用它(現在或未來),而這可能對你或你的使用者有幫助,現在或未來皆然。

這就是標準的好處...

///

實際操作看看

開啟互動式文件:http://127.0.0.1:8000/docs

驗證身分

點選「Authorize」按鈕。

使用下列帳密:

User: johndoe

Password: secret

在系統中完成驗證後,你會看到如下畫面:

取得自己的使用者資料

現在使用 GET 方法呼叫路徑 /users/me

你會取得自己的使用者資料,如:

{
  "username": "johndoe",
  "email": "[email protected]",
  "full_name": "John Doe",
  "disabled": false,
  "hashed_password": "fakehashedsecret"
}

如果你點擊鎖頭圖示登出,然後再次嘗試相同操作,你會得到 HTTP 401 錯誤:

{
  "detail": "Not authenticated"
}

未啟用的使用者

現在改用一個未啟用的使用者,使用以下帳密驗證:

User: alice

Password: secret2

然後再呼叫 GET 方法的 /users/me

你會得到「Inactive user」的錯誤,例如:

{
  "detail": "Inactive user"
}

小結

你現在已經有足夠的工具,能為你的 API 以 usernamepassword 實作一個完整的安全性系統。

使用這些工具,你可以讓安全性系統相容於任何資料庫,以及任何使用者或資料模型。

唯一尚未補上的細節是:它現在其實還不「安全」。

在下一章,你會看到如何使用安全的密碼雜湊函式庫與 JWT tokens。