首页
/ FastAPI 安全实战:HTTP Basic Auth 认证接入与基于 `secrets` 的防时序攻击凭据校验

FastAPI 安全实战:HTTP Basic Auth 认证接入与基于 `secrets` 的防时序攻击凭据校验

2026-09-08 11:41:14作者:农烁颖Land

HTTP Basic Auth 是 Web 认证中历史最悠久、最简单的方案之一。本篇基于 FastAPI 官方文档(韩文版)HTTP Basic Auth 展开,结合仓库中 fastapi.security 的源码实现与测试用例,带你掌握两件事:一是用 HTTPBasic 快速为接口加上 Basic 认证;二是如何在依赖里用 Python 标准库 secrets.compare_digest() 做抗时序攻击(Timing Attack)的用户名/密码校验。读完你即可在 FastAPI 项目中复现一份既可用、又经得起推敲的 Basic Auth 完整方案。

FastAPI Swagger UI 弹出了浏览器内置的用户名/密码登录框,等待输入后才会执行受保护的 /users/me 请求

HTTP Basic Auth 的基本交互流程

在最简单的场景下,HTTP Basic Auth 的协议约定非常直观:

  • 应用期望请求携带一个包含用户名和密码的请求头(即 Authorization 头);
  • 若没有收到该头,应用返回 HTTP 401 "Unauthorized" 错误;
  • 同时返回值为 Basic(可附带可选的 realm 参数)的 WWW-Authenticate 响应头
  • 浏览器收到该头后,会弹出内置的用户名/密码输入框
  • 用户输入并确认后,浏览器会在后续请求中自动把凭据写进 Authorization 头并随请求发送。

也就是说,Basic Auth 下"让浏览器弹框"这一体验并不是前端代码实现的,而是后端通过 401 + WWW-Authenticate 触发的标准浏览器行为。FastAPI 正是利用这一点,把整套协议封装成了开箱即用的安全组件。

最简单的 HTTP Basic Auth 实现

在 FastAPI 中接入 Basic Auth 只需要四步(完整示例见 docs_src/security/tutorial006_an_py310.py):

  1. fastapi.security 导入 HTTPBasicHTTPBasicCredentials
  2. HTTPBasic() 创建一个 "security scheme" 实例;
  3. 路径操作中通过依赖注入(Depends)使用该实例;
  4. 依赖返回一个 HTTPBasicCredentials 类型对象,其中包含请求携带的 usernamepassword
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}

首次在浏览器打开接口地址(或点击 Swagger 文档中的 "Execute")时,浏览器便会弹出上图那样的登录框;只有输入正确的用户名与密码后,请求才会真正发出。这一现象对应本仓库 tests/test_security_http_basic_realm.py 中的断言:带 auth=("john", "secret") 的请求返回 200,且响应体为 {"username": "john", "password": "secret"},证明框架已自动完成 Authorization 头的解析。

底层是如何解析凭据的

之所以返回的依赖对象直接就是 username/password 两个字段,是因为 fastapi/security/http.py 中定义了:

class HTTPBasicCredentials(BaseModel):
    username: Annotated[str, Doc("The HTTP Basic username.")]
    password: Annotated[str, Doc("The HTTP Basic password.")]

而真正的解析发生在 HTTPBasic.__call__ 中(见 fastapi/security/http.py),其核心步骤是:

  1. 读取请求的 Authorization 头,用 fastapi/security/utils.py 中的 get_authorization_scheme_param 按第一个空格切分出 scheme 与参数部分;
  2. 若缺少该头、或 scheme 不是 basic(大小写不敏感),在默认配置下直接抛 401 "Not authenticated" 异常并附带 WWW-Authenticate 头;
  3. 对参数部分执行 base64 解码为 ASCII 字符串;
  4. partition(":") 在首个 : 处拆分出 usernamepassword,若没有 : 分隔符同样判定为未认证;
  5. 解码失败(如传入的并非合法 Base64)时同样返回 401。

值得注意的是:整个流程中用户名/密码只是做了 base64 编码,并非加密,这一点从源码的 b64decode(param) 可直接看出,因此在生产环境必须依赖 HTTPS 保证传输安全。

realm 参数与 OpenAPI 呈现

HTTPBasic 的构造函数支持 scheme_namerealmdescriptionauto_error 四个可选参数。其中 realm 会拼入挑战头中,例如 HTTPBasic(realm="simple") 时,未认证响应的头为 WWW-Authenticate: Basic realm="simple"(见 fastapi/security/http.py),该行为也被 tests/test_security_http_basic_realm.py 明确验证。同时,FastAPI 会把该 scheme 注册进自动生成的 OpenAPI:从同一测试的 test_openapi_schema 可以看到 components.securitySchemes 下出现 {"HTTPBasic": {"type": "http", "scheme": "basic"}},因此 Swagger UI 会自动为接口加上锁形 Authorize 按钮。

用依赖校验用户名与密码:升级版示例

只做"解析"而不做"校验"显然没有意义。更完整的做法是把校验逻辑封装进一个依赖函数(完整示例见 docs_src/security/tutorial007_an_py310.py):

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}

其中几个细节值得展开:

  • 依赖函数接收 HTTPBasicCredentials,随后与硬编码的期望值 "stanleyjobson" / "swordfish" 比对;真实项目中通常会把期望值换成查询数据库或配置项的结果。
  • secrets.compare_digest() 要求输入为 bytes,或者仅包含 ASCII 字符str(即英文范围内的字符)。像 Sebastián 中的 á 这类非 ASCII 字符会导致调用失败,因此示例先调用 .encode("utf8") 把输入统一转成字节串再比较。
  • 校验失败时抛出状态码 401 的 HTTPException(与未提供凭据时一致),并附带 WWW-Authenticate: Basic 头,让浏览器再次弹出登录框。

表面上看,这段逻辑等价于朴素的字符串比较:

if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"):
    # 返回某个错误
    ...

但两者在安全语义上完全不同,差别正是文档接下来重点阐述的"时序攻击"。

什么是时序攻击(Timing Attack)

设想攻击者在逐步猜测用户名与密码:先发送用户名 johndoe、密码 love123,此时应用代码的执行近似于:

if "johndoe" == "stanleyjobson" and "love123" == "swordfish":
    ...

Python 比较字符串时按字符逐位进行,一旦发现首字符 js 不同就立刻短路返回 False——它认为"没必要再浪费计算去比较剩余字符",随后应用返回 Incorrect username or password

当攻击者改用用户名 stanleyjobsox、密码不变时,应用代码近似于:

if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish":
    ...

这一次,Python 必须把 stanleyjobsoxstanleyjobson 共同的 stanleyjobso 全部比较完,才会在最后一个字符处判定不相等,因此返回同样的错误提示会多花几个微秒

响应耗时成为攻击者的"信息通道"

关键在于:当攻击者观察到服务端这次回复 Incorrect username or password 多用了几个微秒,就能推断出前几个字符猜对了——自己的输入与正确答案的相似度,远高于 johndoe。于是下一次他们会尝试更接近 stanleyjobsox 的候选值。

"职业级"攻击方式

现实中的攻击者当然不会手工逐字符试错,而是编写自动化程序,以每秒数千甚至数百万次的速率发起请求,每次只"套取"一个正确的字符。这样持续几分钟到几小时,仅凭响应耗时这一侧信道,攻击者就能在应用代码"配合"下逐步还原出正确的用户名与密码。

secrets.compare_digest() 如何修复

由于示例代码实际使用的是 secrets.compare_digest(),情况就完全不同了:无论比较 stanleyjobsoxstanleyjobson,还是比较 johndoestanleyjobson,其耗时都保持一致,密码字段同理。耗时与内容的匹配程度解耦后,攻击者便无法再从响应时间中提取任何有用信息——整个这类基于时间推断的攻击面随之被消除。

凭据错误时如何返回 401

校验发现凭据不正确后,应返回状态码 401 的 HTTPException,并且必须带上 WWW-Authenticate,否则浏览器不会再次弹出登录框,用户只能停留在错误页面。这与未提供凭据时 FastAPI 自动生成的 401 使用同一状态码,保证了两种失败场景对客户端的语义一致;区别在于业务校验失败时使用更明确的 detail="Incorrect username or password",避免泄露"到底是用户名错还是密码错"。

可选认证与自动报错开关:auto_error

HTTPBasic 默认在未收到合法凭据时直接中断请求并返回 401(即 auto_error=True,来自 fastapi/security/http.py 的实现说明)。当希望认证是可选的(例如"允许匿名访问,但带凭据则识别身份",或与 HTTP Bearer 等多认证方式并存)时,可构造 HTTPBasic(auto_error=False),此时拿不到凭据时依赖的返回值为 None 而非抛异常,由业务代码自行决定后续逻辑。仓库中的 tests/test_security_http_basic_optional.py 即为该可选行为的回归测试,可作为参考。

用仓库测试验证完整行为

除了上面的原理分析,FastAPI 仓库自带的测试也为整套行为提供了可复现的验证依据。tests/test_security_http_basic_realm.py 中至少覆盖了四条关键路径:

  • 携带合法 Basic 凭据(auth=("john", "secret"))→ 200,返回解析出的用户名与密码;
  • 不携带凭据 → 401,响应体 {"detail": "Not authenticated"}WWW-Authenticate 头为 Basic realm="simple"
  • 携带非法的 Base64 值 → 401 且附带挑战头;
  • Base64 内容中没有 : 分隔符(johnsecret)→ 同样 401;
  • /openapi.json 中按预期生成 type: httpscheme: basicHTTPBasic security scheme。

这组测试与 docs/ko/docs/advanced/security/http-basic-auth.md(英文原版见 docs/en/docs/advanced/security/http-basic-auth.md)中描述的行为一一对应。若你想脱离浏览器自行验证,也可以手工构造头:先把 用户名:密码 做 Base64 编码后作为 Authorization: Basic <编码结果> 发送,FastAPI 端会按上文所述流程完成解码与拆分。

小结与安全提醒

把整套思路串起来,一个健壮的 FastAPI Basic Auth 方案应包含三层:

  1. 接入层:用 HTTPBasic(可配 realm)作为依赖,让框架替你完成 401 挑战头与 Authorization 解析;
  2. 校验层:在依赖函数中用 secrets.compare_digest() 对字节串做常量时间比较,杜绝时序攻击;
  3. 失败层:校验失败时抛 401 HTTPException 并显式返回 WWW-Authenticate 头,维持浏览器弹框体验。

最后务必牢记两点:其一,HTTP Basic 的凭据仅经 Base64 编码,是明文等价物,必须运行在 HTTPS 之上;其二,Basic Auth 适用于内部工具、简单网关等低敏场景,涉及高价值资源时应优先考虑 OAuth2、Bearer Token 等更完善的认证体系——但它们交互与实现上的复杂度,也正好反衬出本文这套方案在"够用且安全"这一平衡点上的价值。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389