首页
/ FastAPI 安全教程深入解析:通过依赖注入在路径操作中获取当前用户

FastAPI 安全教程深入解析:通过依赖注入在路径操作中获取当前用户

2026-09-06 13:19:24作者:房伟宁

本篇技术文章基于 FastAPI 官方安全教程中的「获取当前用户」(Aktuellen Benutzer abrufen)一章展开。在上一节中,OAuth2PasswordBearer 依赖已经能为路径操作注入一个 str 类型的 token,但 token 本身对业务逻辑毫无价值——本节将演示如何把「拿到 token」升级为「拿到当前用户」:定义 Pydantic User 模型、创建带有子依赖的 get_current_user 依赖,并把用户对象直接注入到路径操作函数中。读完本文,你将掌握 FastAPI 安全体系中依赖链(依赖的依赖)的构建方式、OpenAPI 安全方案的自动生成机制,以及从源码层面理解 token 校验、401 错误响应与 WWW-Authenticate 头的产生过程。

起点回顾:token 还只是字符串

在上一章(安全初体验)中,我们创建了 OAuth2PasswordBearer 实例并把它作为依赖使用:

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}

上述代码对应仓库中的 tutorial001_an_py310.py。此时依赖系统把 Authorization: Bearer <token> 头中的 token 以字符串形式交给了 read_items,但这仍然不够实用——业务代码需要的是「当前用户是谁」,而不是一段 opaque 的字符串。

创建 Pydantic 用户模型

首先定义一个 Pydantic 用户模型。正如使用 Pydantic 声明请求体(Request Body)一样,它可以用于安全体系的任何位置:

class User(BaseModel):
    username: str
    email: str | None = None
    full_name: str | None = None
    disabled: bool | None = None

完整代码见 tutorial002_an_py310.py(第 12–16 行)。这里有两个值得注意的设计选择:

  • username 是唯一必填字段,emailfull_namedisabled 均为可选字段(带 None 默认值)。disabled 这种布尔字段在真实场景中可以用于在 get_current_user 中拦截已禁用账号;
  • 模型只是普通 Pydantic BaseModel,不涉及数据库——它既可以是从 JWT 解析出来的数据,也可以是 ORM 对象的序列化结果,FastAPI 不关心其来源。

创建带子依赖的 get_current_user 依赖

回忆一下:依赖可以有子依赖。现在创建 get_current_user 依赖,它自身又依赖之前创建的 oauth2_scheme。与上一节在路径操作中直接使用 oauth2_scheme 一样,新依赖 get_current_user 通过子依赖 oauth2_scheme 拿到 str 类型的 token

async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
    ...

这里形成了一条清晰的依赖链:路径操作 → get_current_useroauth2_schemeOAuth2PasswordBearer 实例)。FastAPI 的依赖解析器会自底向上执行这条链:先由 oauth2_scheme 从请求头中提取(并校验格式)Bearer token,再把 token 注入 get_current_usertoken 参数。

获取用户:从 token 解码出 User

get_current_user 使用一个(本例中伪装的)辅助函数把 token 解码为用户模型。该辅助函数接收 str 类型的 token,返回 Pydantic User 模型:

def fake_decode_token(token):
    return User(
        username=token + "fakedecoded", email="john@example.com", full_name="John Doe"
    )


async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
    user = fake_decode_token(token)
    return user

对应源码见 tutorial002_an_py310.py(第 19–27 行)。在真实项目中,fake_decode_token 的位置会被 JWT 验证、数据库查询或远程身份服务调用所替代;而 get_current_user 作为「安全适配层」的职责不变:把 token 翻译成业务世界里的用户对象,并可以在这里处理「token 无效」与「账号已禁用」等异常分支(例如抛出 401/403 的 HTTPException)。

把当前用户注入路径操作

最后,再次在路径操作中使用 Depends(get_current_user),把整个链条接入端点:

@app.get("/users/me")
async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]):
    return current_user

注意 current_user 的声明类型是 Pydantic 模型 User。这个类型标注让代码补全和静态类型检查在函数体内都能识别 current_user.usernamecurrent_user.email 等属性。

Tip:你可能会记得,请求体(Request Body)也是用 Pydantic 模型声明的。但因为你这里使用了 DependsFastAPI 不会混淆「Pydantic 类型 + Depends」和「Pydantic 类型 = 请求体」这两种情况——依赖注入优先级由 Depends 显式声明决定。

Tip:依赖系统的设计允许存在多个不同的依赖(多个 "Dependable")都返回 User 模型。你不必只有一个能返回这种数据的依赖,例如可以为「Web 用户」「API 密钥调用方」各建一个返回 User 的依赖,在需要时自由组合。

完整可运行示例

将以上代码组合起来,就是 docs_src/security/tutorial002_an_py310.py 的完整内容:

from typing import Annotated

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


class User(BaseModel):
    username: str
    email: str | None = None
    full_name: str | None = None
    disabled: bool | None = None


def fake_decode_token(token):
    return User(
        username=token + "fakedecoded", email="john@example.com", full_name="John Doe"
    )


async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
    user = fake_decode_token(token)
    return user


@app.get("/users/me")
async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]):
    return current_user

可以用 uv run fastapi dev 启动,随后访问 http://127.0.0.1:8000/docs 在 Swagger UI 的 Authorize 按钮中输入任意 username/password 获取 token(下一章会实现真正的 /token 端点),再调用 /users/me 观察返回的 User JSON。

源码深入:OAuth2PasswordBearer 到底做了什么

上一节在依赖链底层的 OAuth2PasswordBearer 实例,其运行逻辑全部位于 fastapi/security/oauth2.py 中。理解它的实现,能解释本节所有行为的来源。

Bearer 头的解析与 401 错误

OAuth2PasswordBearer 继承自 OAuth2,而 OAuth2 又继承自 SecurityBaseoauth2.py 第 330 行起的类注释明确说明了这条继承链)。它的 __call__ 方法(第 536–544 行)是每次依赖被调用时执行的逻辑:

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

其中 get_authorization_scheme_param 是一个非常小的纯函数,位于 fastapi/security/utils.py,用 str.partition(" ")Authorization 头的值拆成 scheme(如 Bearer)和 param(token 本体)两部分。因此:

  • 请求携带 Authorization: Bearer testtoken → 依赖返回 testtoken 字符串,交给 get_current_user
  • 请求缺少 Authorization 头,或 scheme 不是 Bearer → 抛出 make_not_authenticated_error()(第 401–421 行)构造的 HTTPException:状态码 401 UNAUTHORIZED,detail 为 "Not authenticated",并附带响应头 WWW-Authenticate: Bearer
  • 构造 OAuth2PasswordBearer 时若传入 auto_error=False,缺少授权头时依赖返回 None 而非报错——这是实现「可选认证」(例如支持 OAuth2 或 Cookie 多种方式的端点)的开关。

仓库测试对行为的精确验证

上述源码行为被 tests/test_tutorial/test_security/test_tutorial002.py 精确断言覆盖,可作为「可验证依据」:

def test_no_token(client: TestClient):
    response = client.get("/users/me")
    assert response.status_code == 401, response.text
    assert response.json() == {"detail": "Not authenticated"}
    assert response.headers["WWW-Authenticate"] == "Bearer"


def test_token(client: TestClient):
    response = client.get("/users/me", headers={"Authorization": "Bearer testtoken"})
    assert response.status_code == 200, response.text
    assert response.json() == {
        "username": "testtokenfakedecoded",
        "email": "john@example.com",
        "full_name": "John Doe",
        "disabled": None,
    }

这两个用例分别验证了「无 token 时 401 + WWW-Authenticate: Bearer」和「携带 Bearer testtoken 时,fake_decode_token 生成的 User 被序列化返回」两条路径,与本节示例的行为一一对应。

OpenAPI 集成:安全方案自动生成

该测试文件中的 test_openapi_schema 快照还揭示了一个重要事实:因为 oauth2_scheme 继承自 SecurityBase,FastAPI 会自动在 /openapi.json 中生成安全方案声明,无需任何手工配置:

"security": [{"OAuth2PasswordBearer": []}],
"components": {
    "securitySchemes": {
        "OAuth2PasswordBearer": {
            "type": "oauth2",
            "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}
        }
    }
}

也就是说,tokenUrl="token" 这个参数并非创建 /token 端点,而是向 OpenAPI 声明「客户端应到相对 URL token 处获取令牌」——这正是 Swagger UI 中出现 Authorize 按钮和登录表单的原因。tokenUrl 采用驼峰命名是刻意与 OpenAPI 规范保持一致,方便直接对照规范查阅(见 first-steps.md 中的说明)。

不限于 Pydantic 模型:返回什么完全由你决定

现在你可以在路径操作函数中直接拿到当前用户,并在 Dependency Injection 层用 Depends 处理安全机制,同时使用满足安全需求的所有模型和数据(本例为 Pydantic User 模型)。

但 FastAPI 并不限制你使用特定的数据模型、类或类型:

  • 想只保留 idemail、不要 username?没问题,用同样的工具改模型即可;
  • 只想要一个 str?或者一个 dict?或者直接返回数据库 ORM 类的模型实例?都一样工作;
  • 你的应用根本没有「登录用户」,只有机器人、Bots 或其他只持有访问令牌的第三方系统?同样适用——依赖链的最后一环返回什么,路径操作就拿到什么。

本质上,你只需为应用选择任意需要的模型、任意数据库、任意类,FastAPI 的依赖注入系统都能承载。

代码规模视角:安全代码只写一次

这个示例看起来代码不少,但那是因为它把安全、数据模型、辅助函数和路径操作全部混在同一个文件里。关键在于:

安全和依赖注入的代码只编写一次。 你可以把它写得任意复杂,但只写一次、放在一处,且保留全部灵活性。而你可以拥有成千上万个复用这套安全体系的路径操作,它们每个都可以只有三行:

@app.get("/users/me")
async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]):
    return current_user

这些路径操作(以及其中任意一部分)都能复用 get_current_user 及你创建的其他依赖。把安全逻辑与端点逻辑分离,正是本节架构的核心收益。

小结

至此,你已经能够在路径操作函数中直接获取「当前用户」——完成了安全搭建的一半。剩下的工作是新增一个路径操作,让客户端(用户)能够真正发送 usernamepassword 来换取 token,该端点将使用 OAuth2PasswordRequestForm 依赖收集表单数据,相关内容见 Simple OAuth2 with Password and Bearer

参考文档入口:get-current-user.md(原文档)、docs/de/docs/tutorial/security/index.md(安全章节总览)。

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