FastAPI HTTP Basic Auth 实战:从最简认证实现到用secrets.compare_digest()抵御 Timing Attack
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
HTTP Basic Auth 是协议级最朴素的一种认证方式:应用期望客户端在请求头里携带用户名与密码,若缺失则返回 401 并引导浏览器弹出登录框。本文基于 FastAPI 官方安全教程(对应仓库文档 docs/es/docs/advanced/security/http-basic-auth.md),结合本仓库中HTTPBasic/HTTPBasicCredentials的真实源码与配套测试,由浅入深讲清两层内容:如何用 FastAPI 快速接入 HTTP Basic Auth;以及当你在代码里手工比对用户名密码时,如何用 Python 标准库secrets.compare_digest()避免引入致命的Timing Attack(时序侧信道攻击)。
HTTP Basic Auth 的工作原理
HTTP Basic Auth 本身是一个很"简单直接"的协议,其完整交互流程如下:
- 客户端(通常是浏览器)向受保护接口发起请求,此时请求里没有任何凭据。
- 服务端识别到需要认证,返回HTTP 401 "Unauthorized"错误。
- 同时,服务端会返回一个
WWW-Authenticate响应头,值为Basic,并可附带一个可选的realm参数(用于提示认证所属区域),例如WWW-Authenticate: Basic realm="admin"。 - 浏览器收到这个头后,会弹出浏览器自带的用户名/密码输入框(而非页面自定义表单)。
- 用户输入后,浏览器把凭据自动编码进请求头再次发起请求,无需前端代码参与。
⚠️ 适用前提:HTTP Basic Auth 只把
username:password做 Base64 编码后放入Authorization头,Base64 不是加密,等同于明文传输。因此官方建议它只用于"最简单的情形",实际生产环境务必配合 HTTPS(TLS)使用,或优先选择 OAuth2、JWT 等更完善的方案。
最简单的 HTTP Basic Auth 实现
要在 FastAPI 中启用 HTTP Basic Auth,只需四步:
- 导入
HTTPBasic与HTTPBasicCredentials; - 用
HTTPBasic()创建一个 "security scheme"(安全方案实例); - 在path operation中通过
Depends(security)把它声明为依赖; - 依赖注入的结果就是一个
HTTPBasicCredentials对象,其中包含客户端提交的username与password字段。
本仓库的示例源码位于 docs_src/security/tutorial006_an_py310.py:
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}仓库同时提供了不使用Annotated的等价格式 docs_src/security/tutorial006_py310.py,函数签名写作credentials: HTTPBasicCredentials = Depends(security)。两种写法语义完全相同,Annotated形式更利于后期扩展默认值与元数据。
当你第一次访问http://127.0.0.1:8000/users/me,或在自动生成的交互式 API 文档中点击 "Execute" 按钮时,浏览器会弹出登录框要求输入用户名与密码:
提示:由于
security依赖被注入到了路径操作函数中,FastAPI 会自动把该安全方案写进 OpenAPI(可在/docs或/openapi.json查看)。对应测试 tests/test_tutorial/test_security/test_tutorial006.py 里用snapshot断言了生成的 schema:securitySchemes中会出现{"HTTPBasic": {"type": "http", "scheme": "basic"}}。
深入源码:HTTPBasic内部做了什么
仅仅使用依赖注入很难看清协议细节,我们直接看底层实现 fastapi/security/http.py(源码对应类HTTPBasic,从HTTPBase继承):
HTTPBasicCredentials(http.py)是一个 PydanticBaseModel,字段只有两个:username: str与password: str,即认证依赖最终的注入结果。- 构造参数
HTTPBasic(realm=None, auto_error=True):realm用于自定义WWW-Authenticate头中的提示区域;auto_error默认True,表示未提供有效凭据时直接抛出 401 错误并中断请求,若设为False则注入结果为None(适合做"可选认证")。 - 在
__call__(http.py)中,FastAPI 依次完成:- 从请求头读取
Authorization; - 借助
get_authorization_scheme_param()(见 fastapi/security/utils.py)以空格切分出 scheme 与参数部分,并校验 scheme 是否为basic(不区分大小写); - 对参数部分做 Base64 解码并按 ASCII 解码;任何
ValueError、UnicodeDecodeError或binascii.Error(例如传入Basic notabase64token)都会被统一转成 401 错误; - 用
partition(":")按第一个冒号拆出username与password;若没有冒号分隔符同样判定为未认证; - 最终返回
HTTPBasicCredentials(username=..., password=...)交给你的路径操作函数。
- 从请求头读取
也就是说,协议层解析(编解码、报错、头生成)FastAPI 已替你完成,你需要关心的只是"拿到的用户名密码对不对"。
校验用户名与密码:引入secrets.compare_digest()
拿到credentials后,最直接的写法是普通相等比较:
if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"): # 返回某个错误 ...但这样写存在安全隐患(详见下文 Timing Attack 一节)。更完整的官方示例位于 docs_src/security/tutorial007_an_py310.py(无Annotated版本见 docs_src/security/tutorial007_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}这里有两个容易被忽略的工程细节:
secrets.compare_digest()要求参数为bytes,或仅含 ASCII 字符的str。若用户名含á(如Sebastián)这类非 ASCII 字符,直接传入会报错。因此代码先调用credentials.username.encode("utf8")把输入转成 UTF-8 字节,再与b"stanleyjobson"这类字节常量做常量时间比较。- 校验失败时抛出的
HTTPException携带两个关键信息:状态码 401(与"未提供凭据"时一致,避免泄露"用户存在与否"的信息),以及响应头{"WWW-Authenticate": "Basic"}。带上该头,浏览器才知道要再次弹出登录框,而不是直接展示错误页。
Timing Attack:为什么不能直接==
攻击原理:比较耗时本身就是信息
假设攻击者正在暴力猜测用户名与密码,先发来johndoe/love123。你的 Python 校验代码等价于:
if "johndoe" == "stanleyjobson" and "love123" == "swordfish": ...Python 比较字符串时一旦发现第一个字符不同(jvss),就会立刻返回False——它认为"没必要再浪费算力比较剩余字符"。于是这次请求很快结束,应用返回"用户名或密码错误"。
接着攻击者改试stanleyjobsox/love123,代码变成:
if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish": ...这次 Python 必须逐字符比较完前 12 个字符stanleyjobso才会发现两者不同,因此响应会多花费几微秒。攻击者捕捉到这个细微差异,就获得了信息:我猜中的某些开头字母是正确的。
"职业级"攻击的威力
攻击者当然不会手工逐字试,而是写脚本以每秒成千上万甚至上百万次的速度发起请求,每次只多猜对一个字符。借助响应时间这个"免费信号",几分钟到几小时内,用户名和密码就可能被完整还原——而这完全是应用自身通过"不同的比较耗时"泄露出去的。
用常量时间比较修复
secrets.compare_digest()解决的正是这个问题:它保证无论两个值在何处首次出现差异,比较所消耗的时间都基本相同。也就是说,比较stanleyjobsox与stanleyjobson的耗时,和比较johndoe与stanleyjobson几乎一致,密码的比较同理。这样一来,攻击者从响应时间上得不到任何可用于逐位逼近的信息,从而对该类安全攻击免疫。
运行与验证
将上述任一示例保存后,可在仓库根目录用uvicorn启动验证(示例文件名以docs_src.security.为模块前缀):
uvicorn docs_src.security.tutorial007_an_py310:app --reload随后访问http://127.0.0.1:8000/docs打开交互文档即可手动体验登录流程。
仓库配套的自动化测试完整覆盖了正确 / 错误凭据场景,见 tests/test_tutorial/test_security/test_tutorial007.py:
- 携带正确凭据
("stanleyjobson", "swordfish")时返回 200,响应体为{"username": "stanleyjobson"}; - 不带凭据、携带非法 Base64、用户名错(
alice)或密码错(wrongpassword)时,均返回 401,且响应头带WWW-Authenticate: Basic,响应体为{"detail": "Incorrect username or password"}(未提供凭据时为"Not authenticated"); - OpenAPI schema 校验与 test_tutorial006.py 一致,都会在
/users/me的security声明HTTPBasic方案。
小结
本文覆盖了 FastAPI 中 HTTP Basic Auth 的完整链路:协议层的401+WWW-Authenticate交互、HTTPBasic依赖的底层编解码与错误处理、基于secrets.compare_digest()的常量时间凭据校验,以及 Timing Attack 从原理到防御的完整推导。需要再次强调的是:HTTP Basic Auth 属于"最简单场景"的认证手段,仅适合内部工具、临时调试等低敏环境;任何面向公网的服务都应在 HTTPS 之上设计更完善的认证方案,而本文中"用依赖封装校验 + 常量时间比较 + 统一的 401 响应"的工程范式,即使换用 OAuth2 等方案也依然值得沿用。
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考