FastAPI 安全入门:用 OAuth2 `password` 流与 `OAuth2PasswordBearer` 搭建认证雏形
基于 FastAPI 官方教程「Security - First Steps」(英文原文见 docs/en/docs/tutorial/security/first-steps.md,本文所依据的印地语版本见 docs/hi/docs/tutorial/security/first-steps.md)展开,本文讲解如何用 OAuth2 的 password 流配合 Bearer token,让前后端分离的应用在几分钟内获得可用的认证骨架。读完你将能够:复制运行一段十几行的最小示例,理解 tokenUrl、Authorization: Bearer <token> 请求头的语义,掌握 OAuth2PasswordBearer 的底层实现原理,并知道 FastAPI 是如何把这一安全方案自动写进 OpenAPI 与交互式文档的。
文章所用的示例代码位于 docs_src/security/tutorial001_an_py310.py,全部结论均可通过仓库源码与测试用例验证。
使用场景:前后端分离应用中的登录认证
先设想一个非常典型的架构:
- backend API 部署在某个域名上;
- frontend 部署在另一个域名,或同一个域名的另一个路径下(也可能是某个移动应用);
- 你希望前端用 username 和 password 与后端完成认证。
这正是 OAuth2 擅长的场景。但在绝大多数时候,你并不需要通读整篇冗长的 OAuth2 规范来寻找需要的几个细节——FastAPI 已经把这套安全机制封装成了开箱即用的工具。本教程的目标就是:先让代码跑起来、看到效果,再回头理解背后发生了什么。
先跑起来:创建 main.py
将下面的代码复制到一个名为 main.py 的文件中。这是本教程唯一的示例应用,全部核心逻辑只有十几行:
from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.get("/items/")
async def read_items(token: Annotated[str, Depends(oauth2_scheme)]):
return {"token": token}
代码段中出现了本教程的主角——OAuth2PasswordBearer。它通过 Depends 被注入到路径操作函数 read_items 的参数 token 上,其类型标注为 str。在仓库中还提供了不使用 Annotated 的等价写法 docs_src/security/tutorial001_py310.py,两者行为完全一致。
运行前先装好 python-multipart
这里有一个重要的安装前提。OAuth2 规范规定,username 和 password 必须通过 form data(表单数据) 发送,而不是 JSON,因此需要额外依赖 python-multipart 包来解析这类请求体:
- 使用
uv add "fastapi[standard]"安装 FastAPI 时,python-multipart会被自动带上; - 但如果使用
uv add fastapi(不带[standard]特性),它不会默认安装; - 需要手动补充时,执行:
$ uv add python-multipart
启动服务
用 FastAPI 自带的开发服务器启动应用:
$ uv run fastapi dev
启动成功后,终端会显示 Uvicorn 正在监听:
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
在交互式文档中亲手验证
打开交互式 API 文档地址 http://127.0.0.1:8000/docs,你会发现界面与普通教程完全不同。
注意到两个新元素:
- 页面右上角出现了一个崭新的 Authorize 按钮;
/items/这个路径操作的右上角出现了一把小锁,可以点击。
点击 Authorize 按钮后,会弹出一个小型授权表单,要求填写 username、password 以及其他可选字段:
此时无论你在表单里输入什么,请求暂时都不会成功——因为真正签发 token 的接口还没有实现。但请记住:这个表单并不是给最终用户使用的前端,它只是 FastAPI 为你的整个 API 自动生成的交互式文档工具。它能被前端团队(可能就是你本人)、第三方应用与系统,以及你自己用来调试、检查和测试同一个应用。
OAuth2 password 流:先理解整体流程
回到概念层面。password 流是 OAuth2 规范定义的若干安全与认证方式(flows)之一。OAuth2 在设计上的初衷,是让 backend 或 API 与“负责认证用户的那个服务器”相互独立;但在这个例子里,同一个 FastAPI 应用既承担 API 业务,也承担认证。从这种简化视角出发,整个流程如下:
- 用户在 frontend 输入
username和password,按下回车; - 运行在用户浏览器中的 frontend,把这对凭证发送到我们 API 的某个特定 URL(这个 URL 由
tokenUrl="token"声明); - API 校验
username和password,然后返回一个 “token”(本教程尚未实现任何一步);- “token”本质上只是一串内容,用于之后验证用户身份;
- 通常 token 会被设置为一段时间后过期:
- 因此用户过段时间需要重新登录;
- 即使 token 被盗,风险也更低——它不像永久密钥那样(在大多数情况下)永远有效;
- frontend 把 token 临时存储在某个地方;
- 用户点击 frontend 跳转到其他区域;
- frontend 需要再从 API 获取更多数据,而该接口要求认证:
- frontend 发送一个名为
Authorization的请求头,其值为Bearer加上 token; - 例如 token 是
foobar,请求头内容就是Bearer foobar。
- frontend 发送一个名为
FastAPI 的 OAuth2PasswordBearer:把流程落进代码
FastAPI 为安全功能提供了不同抽象层次的多种工具。本例使用的是:OAuth2 + Password 流 + Bearer token,组合起来就是 OAuth2PasswordBearer 这个类。
关于 “bearer”:Bearer token 并不是唯一选项,但对当前用例而言它是合适的;对绝大多数用例它也可能都是最佳选择——除非你是 OAuth2 专家,明确知道某种替代方案更适合你的需求。即便如此,FastAPI 也提供了构建其他方案的底层工具。
实例化与 tokenUrl 参数
创建 OAuth2PasswordBearer 实例时,必须传入 tokenUrl 参数:
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
这个参数指向“客户端(用户浏览器里的 frontend)为了换取 token 而要发送 username 和 password 的那个 URL”。有三个容易误解的细节需要澄清:
其一,tokenUrl 并不创建该端点。 它只是“声明”URL /token 是客户端换取 token 时应访问的地址。这条信息会被写入 OpenAPI schema,进而被交互式 API 文档使用。真正的路径操作我们稍后会自己实现——在本教程的 main.py 中甚至还没有它。
其二,tokenUrl="token" 是相对 URL。 相对 URL token 等价于 ./token。这意味着:如果 API 位于 https://example.com/,它指向 https://example.com/token;如果 API 位于 https://example.com/api/v1/,它则指向 https://example.com/api/v1/token。坚持使用相对 URL 非常重要,它能保证应用在反向代理之后这类高级部署场景下依然正常工作。
其三,参数名为什么是 tokenUrl 而非 token_url。 如果你是很严格的 “Pythonista”,可能不喜欢这种驼峰命名。这是刻意为之——它沿用了 OpenAPI 规范里的字段名,这样当你需要对任一安全方案做更深入调研时,可以直接把这个名字复制粘贴去检索资料。
oauth2_scheme 是一个可调用对象
oauth2_scheme 变量表面上是 OAuth2PasswordBearer 的实例,但该类实现了 __call__,因此它本身也是 “callable”,可以这样调用:
oauth2_scheme(some, parameters)
这正是它能与 Depends 配合使用的前提——FastAPI 的依赖注入机制要求被注入对象(或其工厂)是可以被调用的。
用 Depends 注入 token
把 oauth2_scheme 放进 Depends,作为路径操作的依赖:
from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.get("/items/")
async def read_items(token: Annotated[str, Depends(oauth2_scheme)]):
return {"token": token}
这条依赖最终会提供一个 str,并赋值给路径操作函数里的参数 token。同时,FastAPI 会识别出:可以用这条依赖在 OpenAPI schema(以及自动生成的 API 文档)中声明一个 “security scheme”。
受保护端点在不带 token 时的行为
请求到达后,依赖会去查找请求中的 Authorization 请求头:
- 检查其值是否为
Bearer加上某个 token,若是则把 token 作为str返回; - 如果看不到
Authorization头,或值里没有Bearertoken,则直接以 401 状态码(UNAUTHORIZED)返回错误。
你甚至不需要自己检查 token 是否存在——只要你的函数被执行了,token 参数里就一定是一个 str。
在交互式文档中可以直接点击 “Try it out” 实测一下:不提供凭证直接请求 /items/,会看到接口返回 401,响应体为 {"detail": "Not authenticated"},同时响应头带有 WWW-Authenticate: Bearer:
注意:到目前为止我们还没有验证 token 的合法性(这是后续教程的主题),但这已经是一个很好的起点。
底层原理:从源码看 FastAPI 如何识别与执行安全方案
仅仅会用还不够,理解源码能让你在需要自定义安全方案时游刃有余。以下依据均来自仓库源码。
继承链:为什么 FastAPI 知道它是 security scheme
技术细节上,FastAPI 之所以能识别 OAuth2PasswordBearer(在依赖中声明的类)可用于定义 OpenAPI 中的 security scheme,是因为它的继承关系:
OAuth2PasswordBearer 继承自 fastapi.security.oauth2.OAuth2(见 fastapi/security/oauth2.py),而后者又继承自 fastapi.security.base.SecurityBase(见 fastapi/security/base.py)。
从源码结构看,SecurityBase 是一个标记性基类,要求子类提供 model: SecurityBaseModel 与 scheme_name: str。所有能与 OpenAPI(及自动 API 文档)集成的安全工具都继承自 SecurityBase,FastAPI 正是以此判断如何把它们集成进 OpenAPI。此外,OAuth2PasswordBearer 在 fastapi/security/init.py 中被重新导出,所以你可以直接从 fastapi.security 导入。
__call__:真正的认证逻辑
OAuth2PasswordBearer 的认证行为在其 __call__ 方法中实现(见 fastapi/security/oauth2.py):
async def __call__(self, request: Request) -> str | None:
authorization = request.headers.get("Authorization")
scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer":
if self.auto_error:
raise self.make_not_authenticated_error()
else:
return None
return param
步骤拆解:
- 从请求头中读取
Authorization; - 调用 fastapi/security/utils.py 中的
get_authorization_scheme_param,用partition(" ")把请求头拆成scheme与param两部分,例如Bearer foobar会得到("Bearer", "foobar"); - 若没有请求头,或 scheme(小写后)不等于
"bearer",则依据auto_error决定行为:默认auto_error=True时抛出未认证错误;设为False时返回None,可用于实现可选认证或多方式认证(例如 OAuth2 与 Cookie 二选一)。
401 错误的构造
未认证错误由 make_not_authenticated_error 构造(见 fastapi/security/oauth2.py),返回一个 HTTPException:
- 状态码:
HTTP_401_UNAUTHORIZED(401); detail:"Not authenticated";- 响应头:
{"WWW-Authenticate": "Bearer"}。
其中 WWW-Authenticate: Bearer 是 HTTP 规范要求的认证质询(challenge)头,提示客户端应以 Bearer 方式携带凭证重试。该错误会由 FastAPI 的 HTTPException 处理机制转换为标准 JSON 响应体 {"detail": "Not authenticated"}。
用测试用例验证上述所有行为
仓库为这个示例配套了完整的测试 tests/test_tutorial/test_security/test_tutorial001.py,覆盖了上文的全部论断:
- 不带 token 请求:
GET /items返回 401,响应体为{"detail": "Not authenticated"},响应头WWW-Authenticate为Bearer; - 携带正确 Bearer token:请求头
Authorization: Bearer testtoken时返回 200,响应体为{"token": "testtoken"}; - scheme 不正确:
Authorization: Notexistent testtoken(非Bearer)同样返回 401 与相同的错误体、响应头; - OpenAPI schema 快照:
/openapi.json中,/items/路径操作带有"security": [{"OAuth2PasswordBearer": []}],而components.securitySchemes下生成的是:
"OAuth2PasswordBearer": {
"type": "oauth2",
"flows": {"password": {"scopes": {}, "tokenUrl": "token"}}
}
可以看到,tokenUrl="token" 被精确地写入了 OpenAPI 的 flows.password.tokenUrl,这正是交互式文档中 Authorize 按钮、锁定图标与授权表单能够自动出现的根本原因。
值得补充的是,FastAPI 为“可选 Bearer 认证”还提供了独立测试(见 tests/test_security_oauth2_password_bearer_optional.py 等),对应 auto_error=False 时依赖返回 None 而非抛出 401 的语义。
Recap:几行代码换来的安全雏形
回顾整个过程:在示例 main.py 的基础上,你实际上只需再增加 3~4 行代码,就获得了一种原始但真实可用的安全形式——受保护的路由会强制校验 Authorization: Bearer <token> 请求头,缺失或格式错误时自动返回标准化的 401 响应,并且这一切都同步反映到了 OpenAPI 与自动生成的交互式文档中。
诚然,这还不是完整的安全方案:token 的签发接口、token 内容与有效期的校验、密码哈希等均未涉及。但你已经掌握了 FastAPI 安全机制的“最小骨架”,理解了 tokenUrl、Depends 注入与 SecurityBase 体系如何协作——这正是继续深入 OAuth2 密码流完整实现(含真实 token 校验)的坚实基础。想要继续探索,可查阅同目录下的 security 教程源码示例 以及英文官方教程正文。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00


