HTTP 基本认证¶
对于最简单的场景,你可以使用 HTTP 基本认证。
在 HTTP 基本认证中,应用程序期望收到包含用户名和密码的请求头。
如果未收到该请求头,则返回 HTTP 401 "Unauthorized" 错误。
并返回值为 Basic 的 WWW-Authenticate 头,以及可选的 realm 参数。
这会告知浏览器显示集成式的用户名和密码输入提示。
然后,当你输入用户名和密码时,浏览器会自动将其包含在请求头中发送。
简单 HTTP 基本认证¶
- 导入
HTTPBasic和HTTPBasicCredentials。 - 使用
HTTPBasic创建一个“安全方案”。 - 在路径操作中使用该
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}
🤓 Other versions and variants
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}
Tip
Prefer to use the Annotated version if possible.
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}
当你首次尝试打开该 URL(或在文档中点击 "Execute" 按钮)时,浏览器会要求你输入用户名和密码:

检查用户名¶
这是一个更完整的示例。
使用依赖项来检查用户名和密码是否正确。
为此,使用 Python 标准模块 secrets 来检查用户名和密码。
secrets.compare_digest() 需要接收 bytes 或仅包含 ASCII 字符(英文字符)的 str,这意味着它无法处理像 Sebastián 中的 á 这样的字符。
为了处理这种情况,我们首先将 username 和 password 转换为使用 UTF-8 编码的 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}
🤓 Other versions and variants
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}
Tip
Prefer to use the Annotated version if possible.
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"):
# 返回某些错误
...
但通过使用 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}
🤓 Other versions and variants
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}
Tip
Prefer to use the Annotated version if possible.
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}