HTTP 基本驗證¶
對於最簡單的情況,您可以使用 HTTP 基本驗證。
在 HTTP 基本驗證中,應用程式預期一個包含使用者名稱和密碼的標頭。
如果沒有收到,它會返回 HTTP 401「未授權」錯誤。
並返回一個 WWW-Authenticate
標頭,其值為 Basic
,以及一個可選的 realm
參數。
這會告知瀏覽器顯示整合的使用者名稱和密碼提示。
然後,當您輸入該使用者名稱和密碼時,瀏覽器會自動在標頭中發送它們。
簡單的 HTTP 基本驗證¶
- 匯入
HTTPBasic
和HTTPBasicCredentials
。 - 使用
HTTPBasic
建立一個「security
模式」。 - 在您的*路徑操作*中,將該
security
與依賴項一起使用。 - 它會返回一個
HTTPBasicCredentials
類型的物件。- 它包含發送的
username
和password
。
- 它包含發送的
from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.security import HTTPBasic, HTTPBasicCredentials
app = FastAPI()
security = HTTPBasic()
@app.get("/users/me")
def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]):
return {"username": credentials.username, "password": credentials.password}
from fastapi import Depends, FastAPI
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from typing_extensions import Annotated
app = FastAPI()
security = HTTPBasic()
@app.get("/users/me")
def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]):
return {"username": credentials.username, "password": credentials.password}
提示
如果可能,建議使用 Annotated
版本。
from fastapi import Depends, FastAPI
from fastapi.security import HTTPBasic, HTTPBasicCredentials
app = FastAPI()
security = HTTPBasic()
@app.get("/users/me")
def read_current_user(credentials: HTTPBasicCredentials = Depends(security)):
return {"username": credentials.username, "password": credentials.password}
當您第一次嘗試開啟網址(或點擊文件中的「執行」按鈕)時,瀏覽器會要求您輸入使用者名稱和密碼。
檢查使用者名稱¶
這是一個更完整的範例。
使用依賴項來檢查使用者名稱和密碼是否正確。
為此,請使用 Python 標準模組 secrets
來檢查使用者名稱和密碼。
secrets.compare_digest()
需要採用 bytes
或僅包含 ASCII 字元(英文中的字元)的 str
,這表示它不適用於像 á
這樣的字元,例如在 Sebastián
中。
為了處理這個問題,我們首先使用 UTF-8 編碼將 username
和 password
轉換為 bytes
。
然後我們可以使用 secrets.compare_digest()
來確保 credentials.username
是 "stanleyjobson"
,並且 credentials.password
是 "swordfish"
。
import secrets
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
app = FastAPI()
security = HTTPBasic()
def get_current_username(
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
current_username_bytes = credentials.username.encode("utf8")
correct_username_bytes = b"stanleyjobson"
is_correct_username = secrets.compare_digest(
current_username_bytes, correct_username_bytes
)
current_password_bytes = credentials.password.encode("utf8")
correct_password_bytes = b"swordfish"
is_correct_password = secrets.compare_digest(
current_password_bytes, correct_password_bytes
)
if not (is_correct_username and is_correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
@app.get("/users/me")
def read_current_user(username: Annotated[str, Depends(get_current_username)]):
return {"username": username}
import secrets
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from typing_extensions import Annotated
app = FastAPI()
security = HTTPBasic()
def get_current_username(
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
current_username_bytes = credentials.username.encode("utf8")
correct_username_bytes = b"stanleyjobson"
is_correct_username = secrets.compare_digest(
current_username_bytes, correct_username_bytes
)
current_password_bytes = credentials.password.encode("utf8")
correct_password_bytes = b"swordfish"
is_correct_password = secrets.compare_digest(
current_password_bytes, correct_password_bytes
)
if not (is_correct_username and is_correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
@app.get("/users/me")
def read_current_user(username: Annotated[str, Depends(get_current_username)]):
return {"username": username}
提示
如果可能,建議使用 Annotated
版本。
import secrets
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
app = FastAPI()
security = HTTPBasic()
def get_current_username(credentials: HTTPBasicCredentials = Depends(security)):
current_username_bytes = credentials.username.encode("utf8")
correct_username_bytes = b"stanleyjobson"
is_correct_username = secrets.compare_digest(
current_username_bytes, correct_username_bytes
)
current_password_bytes = credentials.password.encode("utf8")
correct_password_bytes = b"swordfish"
is_correct_password = secrets.compare_digest(
current_password_bytes, correct_password_bytes
)
if not (is_correct_username and is_correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
@app.get("/users/me")
def read_current_user(username: str = Depends(get_current_username)):
return {"username": username}
這類似於
if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"):
# Return some error
...
但是通過使用 secrets.compare_digest()
,它可以抵禦一種稱為「計時攻擊」的攻擊。
計時攻擊¶
但什麼是「計時攻擊」?
讓我們想像一些攻擊者正試圖猜測使用者名稱和密碼。
他們發送了一個包含使用者名稱 johndoe
和密碼 love123
的請求。
那麼您應用程式中的 Python 程式碼將等同於
if "johndoe" == "stanleyjobson" and "love123" == "swordfish":
...
但在 Python 比較 johndoe
中的第一個 j
與 stanleyjobson
中的第一個 s
的那一刻,它就會返回 False
,因為它已經知道這兩個字串不同,認為「沒有必要浪費更多計算來比較其餘的字母」。而您的應用程式會顯示「使用者名稱或密碼不正確」。
但接著攻擊者嘗試使用使用者名稱 stanleyjobsox
和密碼 love123
。
而您的應用程式程式碼會執行類似
if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish":
...
Python 將不得不比較 stanleyjobsox
和 stanleyjobson
中的整個 stanleyjobso
,然後才能意識到這兩個字串不同。因此,回覆「使用者名稱或密碼不正確」將需要額外的幾微秒。
回應時間幫助了攻擊者¶
此時,通過注意到伺服器花了額外的幾微秒才發送「使用者名稱或密碼不正確」的回應,攻擊者就會知道他們猜對了*一些*東西,一些初始字母是正確的。
這樣他們就能再次嘗試,因為他們知道正確答案可能更接近 stanleyjobsox
而不是 johndoe
。
「專業」攻擊¶
當然,攻擊者不會手動嘗試所有這些組合,他們會編寫程式來執行,可能每秒進行數千或數百萬次測試。而且他們每次只會多猜對一個字母。
但這樣一來,攻擊者只需幾分鐘或幾小時就能猜到正確的使用者名稱和密碼,而這全靠我們的應用程式在回應時間上「提供協助」。
使用 secrets.compare_digest()
修復¶
但在我們的程式碼中,我們實際上使用了 secrets.compare_digest()
。
簡而言之,比較 stanleyjobsox
和 stanleyjobson
所需的時間與比較 johndoe
和 stanleyjobson
所需的時間相同。密碼也是如此。
這樣,在您的應用程式程式碼中使用 secrets.compare_digest()
,就能防止這類安全攻擊。
返回錯誤¶
偵測到憑證不正確後,返回狀態碼為 401 的 HTTPException
(與未提供憑證時返回的狀態碼相同),並新增 WWW-Authenticate
標頭,讓瀏覽器再次顯示登入提示。
import secrets
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
app = FastAPI()
security = HTTPBasic()
def get_current_username(
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
current_username_bytes = credentials.username.encode("utf8")
correct_username_bytes = b"stanleyjobson"
is_correct_username = secrets.compare_digest(
current_username_bytes, correct_username_bytes
)
current_password_bytes = credentials.password.encode("utf8")
correct_password_bytes = b"swordfish"
is_correct_password = secrets.compare_digest(
current_password_bytes, correct_password_bytes
)
if not (is_correct_username and is_correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
@app.get("/users/me")
def read_current_user(username: Annotated[str, Depends(get_current_username)]):
return {"username": username}
import secrets
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from typing_extensions import Annotated
app = FastAPI()
security = HTTPBasic()
def get_current_username(
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
current_username_bytes = credentials.username.encode("utf8")
correct_username_bytes = b"stanleyjobson"
is_correct_username = secrets.compare_digest(
current_username_bytes, correct_username_bytes
)
current_password_bytes = credentials.password.encode("utf8")
correct_password_bytes = b"swordfish"
is_correct_password = secrets.compare_digest(
current_password_bytes, correct_password_bytes
)
if not (is_correct_username and is_correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
@app.get("/users/me")
def read_current_user(username: Annotated[str, Depends(get_current_username)]):
return {"username": username}
提示
如果可能,建議使用 Annotated
版本。
import secrets
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
app = FastAPI()
security = HTTPBasic()
def get_current_username(credentials: HTTPBasicCredentials = Depends(security)):
current_username_bytes = credentials.username.encode("utf8")
correct_username_bytes = b"stanleyjobson"
is_correct_username = secrets.compare_digest(
current_username_bytes, correct_username_bytes
)
current_password_bytes = credentials.password.encode("utf8")
correct_password_bytes = b"swordfish"
is_correct_password = secrets.compare_digest(
current_password_bytes, correct_password_bytes
)
if not (is_correct_username and is_correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
@app.get("/users/me")
def read_current_user(username: str = Depends(get_current_username)):
return {"username": username}