跳转至

安全 - 第一步

🌐 Translation by AI and humans

This translation was made by AI guided by humans. 🤝

It could have mistakes of misunderstanding the original meaning, or looking unnatural, etc. 🤖

You can improve this translation by helping us guide the AI LLM better.

English version

假设你的后端 API 位于某个域名下。

前端在另一个域名,或同一域名的不同路径(或在移动应用中)。

你希望前端能通过usernamepassword 与后端进行身份验证。

我们可以用 OAuth2FastAPI 中实现它。

但为了节省你的时间,不必为获取少量信息而通读冗长的规范。

我们直接使用 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}
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

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: str = Depends(oauth2_scheme)):
    return {"token": token}

运行

信息

当你使用命令 pip install "fastapi[standard]" 安装 FastAPI 时,python-multipart 包会自动安装。

但是,如果你使用 pip install fastapi,默认不会包含 python-multipart 包。

如需手动安装,请先创建并激活虚拟环境,然后执行:

$ pip install python-multipart

这是因为 OAuth2 使用“表单数据”来发送 usernamepassword

用下面的命令运行示例:

$ fastapi dev main.py

<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

查看

打开交互式文档:http://127.0.0.1:8000/docs

你会看到类似这样的界面:

Authorize 按钮!

页面右上角已经有一个崭新的“Authorize”按钮。

你的路径操作右上角还有一个可点击的小锁图标。

点击它,会弹出一个授权表单,可输入 usernamepassword(以及其它可选字段):

注意

目前无论在表单中输入什么都不会生效,我们稍后就会实现它。

这当然不是面向最终用户的前端,但它是一个很棒的自动化工具,可交互式地为整个 API 提供文档。

前端团队(也可能就是你自己)可以使用它。

第三方应用和系统也可以使用它。

你也可以用它来调试、检查和测试同一个应用。

password

现在回过头来理解这些内容。

password “流”(flow)是 OAuth2 定义的处理安全与身份验证的一种方式。

OAuth2 的设计目标是让后端或 API 与负责用户认证的服务器解耦。

但在这个例子中,FastAPI 应用同时处理 API 和认证。

从这个简化的角度来看看流程:

  • 用户在前端输入 usernamepassword,然后按下 Enter
  • 前端(运行在用户浏览器中)把 usernamepassword 发送到我们 API 中的特定 URL(使用 tokenUrl="token" 声明)。
  • API 校验 usernamepassword,并返回一个“令牌”(这些我们尚未实现)。
    • “令牌”只是一个字符串,包含一些内容,之后可用来验证该用户。
    • 通常,令牌会在一段时间后过期。
      • 因此,用户过一段时间需要重新登录。
      • 如果令牌被窃取,风险也更小。它不像一把永久有效的钥匙(在大多数情况下)。
  • 前端会把令牌临时存储在某处。
  • 用户在前端中点击跳转到前端应用的其他部分。
  • 前端需要从 API 获取更多数据。
    • 但该端点需要身份验证。
    • 因此,为了与我们的 API 进行身份验证,它会发送一个 Authorization 请求头,值为 Bearer 加上令牌。
    • 如果令牌内容是 foobarAuthorization 请求头的内容就是:Bearer foobar

FastAPIOAuth2PasswordBearer

FastAPI 在不同抽象层级提供了多种安全工具。

本示例将使用 OAuth2Password 流程并配合 Bearer 令牌,通过 OAuth2PasswordBearer 类来实现。

信息

“Bearer” 令牌并非唯一选项。

但它非常适合我们的用例。

对于大多数用例,它也可能是最佳选择,除非你是 OAuth2 专家,并明确知道为何其他方案更适合你的需求。

在那种情况下,FastAPI 同样提供了相应的构建工具。

创建 OAuth2PasswordBearer 类实例时,需要传入 tokenUrl 参数。该参数包含客户端(运行在用户浏览器中的前端)用来发送 usernamepassword 以获取令牌的 URL。

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}
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

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: str = Depends(oauth2_scheme)):
    return {"token": token}

提示

这里的 tokenUrl="token" 指向的是尚未创建的相对 URL token,等价于 ./token

因为使用的是相对 URL,若你的 API 位于 https://example.com/,它将指向 https://example.com/token;若你的 API 位于 https://example.com/api/v1/,它将指向 https://example.com/api/v1/token

使用相对 URL 很重要,这能确保你的应用在诸如使用代理等高级用例中依然正常工作。

这个参数不会创建该端点/路径操作,而是声明客户端应使用 /token 这个 URL 来获取令牌。这些信息会用于 OpenAPI,进而用于交互式 API 文档系统。

我们很快也会创建对应的实际路径操作。

信息

如果你是非常严格的 “Pythonista”,可能不喜欢使用参数名 tokenUrl 而不是 token_url

这是因为它使用了与 OpenAPI 规范中相同的名称。这样当你需要深入了解这些安全方案时,可以直接复制粘贴去查找更多信息。

oauth2_scheme 变量是 OAuth2PasswordBearer 的一个实例,同时它也是“可调用”的。

可以像这样调用:

oauth2_scheme(some, parameters)

因此,它可以与 Depends 一起使用。

使用

现在你可以通过 Dependsoauth2_scheme 作为依赖传入。

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}
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

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: str = Depends(oauth2_scheme)):
    return {"token": token}

该依赖会提供一个 str,赋值给路径操作函数的参数 token

FastAPI 会据此在 OpenAPI 架构(以及自动生成的 API 文档)中定义一个“安全方案”。

技术细节

FastAPI 之所以知道可以使用(在依赖中声明的)OAuth2PasswordBearer 在 OpenAPI 中定义安全方案,是因为它继承自 fastapi.security.oauth2.OAuth2,而后者又继承自 fastapi.security.base.SecurityBase

所有与 OpenAPI(以及自动 API 文档)集成的安全工具都继承自 SecurityBase,这就是 FastAPI 能将它们集成到 OpenAPI 的方式。

它做了什么

它会在请求中查找 Authorization 请求头,检查其值是否为 Bearer 加上一些令牌,并将该令牌作为 str 返回。

如果没有 Authorization 请求头,或者其值不包含 Bearer 令牌,它会直接返回 401 状态码错误(UNAUTHORIZED)。

你甚至无需检查令牌是否存在即可返回错误;只要你的函数被执行,就可以确定会拿到一个 str 类型的令牌。

你已经可以在交互式文档中试试了:

我们还没有验证令牌是否有效,但这已经是一个良好的开端。

小结

只需增加三四行代码,你就已经拥有了一种初步的安全机制。