news 2026/9/7 1:44:13

FastAPI HTTP Basic Auth 实战:从最简认证实现到用 `secrets.compare_digest()` 抵御 Timing Attack

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FastAPI HTTP Basic Auth 实战:从最简认证实现到用 `secrets.compare_digest()` 抵御 Timing Attack

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 本身是一个很"简单直接"的协议,其完整交互流程如下:

  1. 客户端(通常是浏览器)向受保护接口发起请求,此时请求里没有任何凭据
  2. 服务端识别到需要认证,返回HTTP 401 "Unauthorized"错误。
  3. 同时,服务端会返回一个WWW-Authenticate响应头,值为Basic,并可附带一个可选的realm参数(用于提示认证所属区域),例如WWW-Authenticate: Basic realm="admin"
  4. 浏览器收到这个头后,会弹出浏览器自带的用户名/密码输入框(而非页面自定义表单)。
  5. 用户输入后,浏览器把凭据自动编码进请求头再次发起请求,无需前端代码参与。

⚠️ 适用前提:HTTP Basic Auth 只把username:password做 Base64 编码后放入Authorization头,Base64 不是加密,等同于明文传输。因此官方建议它只用于"最简单的情形",实际生产环境务必配合 HTTPS(TLS)使用,或优先选择 OAuth2、JWT 等更完善的方案。

最简单的 HTTP Basic Auth 实现

要在 FastAPI 中启用 HTTP Basic Auth,只需四步:

  1. 导入HTTPBasicHTTPBasicCredentials
  2. HTTPBasic()创建一个 "security scheme"(安全方案实例);
  3. path operation中通过Depends(security)把它声明为依赖;
  4. 依赖注入的结果就是一个HTTPBasicCredentials对象,其中包含客户端提交的usernamepassword字段。

本仓库的示例源码位于 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: strpassword: str,即认证依赖最终的注入结果。
  • 构造参数HTTPBasic(realm=None, auto_error=True)realm用于自定义WWW-Authenticate头中的提示区域;auto_error默认True,表示未提供有效凭据时直接抛出 401 错误并中断请求,若设为False则注入结果为None(适合做"可选认证")。
  • __call__(http.py)中,FastAPI 依次完成:
    1. 从请求头读取Authorization
    2. 借助get_authorization_scheme_param()(见 fastapi/security/utils.py)以空格切分出 scheme 与参数部分,并校验 scheme 是否为basic(不区分大小写);
    3. 对参数部分做 Base64 解码并按 ASCII 解码;任何ValueErrorUnicodeDecodeErrorbinascii.Error(例如传入Basic notabase64token)都会被统一转成 401 错误;
    4. partition(":")按第一个冒号拆出usernamepassword;若没有冒号分隔符同样判定为未认证;
    5. 最终返回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()解决的正是这个问题:它保证无论两个值在何处首次出现差异,比较所消耗的时间都基本相同。也就是说,比较stanleyjobsoxstanleyjobson的耗时,和比较johndoestanleyjobson几乎一致,密码的比较同理。这样一来,攻击者从响应时间上得不到任何可用于逐位逼近的信息,从而对该类安全攻击免疫。

运行与验证

将上述任一示例保存后,可在仓库根目录用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/mesecurity声明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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 1:44:08

大模型“纯血自研”真假难辨?从Tokenizer到API行为四步识别套壳模型

这两天行业群被一个消息炸得不轻:中东那边冒出一个号称“纯血自研”的大模型,发布会PPT写得相当有排面,从芯片到框架到训练框架全是我方掌控的气势。结果没热闹两天,就有技术老哥扒出这模型的推理风格、返回结构和语料习惯跟MiniM…

作者头像 李华
网站建设 2026/9/7 1:44:04

人形机器人强化学习导航真机部署:以众擎PM01为例

这次我们来看一个非常具体、也比较硬核的方向:众擎 PM01 人形机器人的强化学习导航真机部署。如果你是做机器人导航、强化学习策略迁移或者 ROS2 真机部署的工程师,这篇文章可以直接收藏。重点不是把强化学习算法再讲一遍,而是把“仿真里训练…

作者头像 李华
网站建设 2026/9/7 1:44:02

千款AI工具汇总背后的选型心法:从收藏到搭建高效工作流

简介:面向人工智能生成内容时代个人与团队效率提升,这份人工智能工具汇总文档收录了一千多款主流人工智能应用,覆盖内容创作、数据分析、自动化办公、智能生活、人工智能绘画、人工智能写作、人工智能视频、人工智能问答等高频场景。每项工具…

作者头像 李华
网站建设 2026/9/7 1:43:06

PerceptionBench多模态视觉基准测试:从环境搭建到结果分析全指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 1:41:09

AI Agent开发实战:让AI倾听往事并自动撰写回忆录

想象这样一个场景:家里的长辈拿起手机,像平常聊天一样随口说了一句“我年轻的时候在厂里当车工,有一年评先进,车间主任把我叫到办公室……”手机对面的 AI 不急着讲道理,而是轻轻应了一句“那后来呢?”等长…

作者头像 李华
网站建设 2026/9/7 1:40:45

野猫湖平台UFS实战:零刻EQ mini搭配长江存储UC341性能功耗解码

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华