跳到内容

安全性 - 初步

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

你有一个 **前端** 位于另一个域名或相同域名的不同路径下(或在移动应用程序中)。

你希望前端能够使用 **用户名** 和 **密码** 向后端进行身份验证。

我们可以使用 **OAuth2** 与 **FastAPI** 来构建它。

但为了节省你阅读冗长完整规范的时间,我们只提取你需要的那部分信息。

让我们使用 **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}
🤓 其他版本和变体
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
from typing_extensions import Annotated

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


@app.get("/items/")
async def read_items(token: Annotated[str, Depends(oauth2_scheme)]):
    return {"token": token}

提示

如果可能,请优先使用 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: str = Depends(oauth2_scheme)):
    return {"token": token}

运行

信息

当你运行 pip install "fastapi[standard]" 命令时,`python-multipart` 包会自动随 **FastAPI** 安装。

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

要手动安装它,请确保你创建了一个虚拟环境,激活它,然后使用以下命令安装:

$ pip install python-multipart

这是因为 **OAuth2** 使用“表单数据”来发送 `username` 和 `password`。

使用以下命令运行示例

$ 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”按钮。

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

如果你点击它,会弹出一个小授权表单,让你输入 `username` 和 `password`(以及其他可选字段)

注意

无论你在表单中输入什么,目前都不会起作用。但我们很快就会实现它。

这当然不是面向最终用户的交互式前端,但它是一个很棒的自动工具,可以交互式地记录你所有的 API。

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

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

你也可以自己使用它来调试、检查和测试同一个应用程序。

密码流

现在让我们稍微回顾一下,理解这一切是什么。

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

OAuth2 的设计目的是使后端或 API 可以独立于验证用户的服务器。

但在此案例中,同一个 **FastAPI** 应用程序将处理 API 和身份验证。

所以,让我们从简化的角度回顾一下

  • 用户在前端输入 `username` 和 `password`,然后按下 `Enter` 键。
  • 前端(在用户浏览器中运行)将 `username` 和 `password` 发送到我们 API 中的特定 URL(使用 `tokenUrl="token"` 声明)。
  • API 检查 `username` 和 `password`,并返回一个“令牌”(我们尚未实现此部分)。
    • “令牌”只是一个包含特定内容的字符串,我们以后可以使用它来验证此用户。
    • 通常,令牌会在一段时间后过期。
      • 因此,用户将在稍后某个时候需要再次登录。
      • 如果令牌被盗,风险也较低。它不像一个永久有效的密钥(在大多数情况下)。
  • 前端将该令牌暂时存储在某个地方。
  • 用户在前端点击以进入前端 Web 应用程序的另一个部分。
  • 前端需要从 API 获取更多数据。
    • 但它需要针对该特定端点进行身份验证。
    • 因此,为了与我们的 API 进行身份验证,它会发送一个 `Authorization` 头部,其值为 `Bearer` 加上令牌。
    • 如果令牌包含 `foobar`,则 `Authorization` 头部的内容将是:`Bearer foobar`。

FastAPI 的 `OAuth2PasswordBearer`

FastAPI 提供了不同抽象级别的多种工具,用于实现这些安全功能。

在此示例中,我们将使用 **OAuth2**,采用 **密码** 流,并使用 **不记名 (Bearer)** 令牌。我们通过 `OAuth2PasswordBearer` 类来实现这一点。

信息

“不记名”令牌不是唯一的选择。

但它最适合我们的用例。

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

在这种情况下,**FastAPI** 也为你提供了构建它的工具。

当我们创建 `OAuth2PasswordBearer` 类的一个实例时,我们会传入 `tokenUrl` 参数。此参数包含客户端(在用户浏览器中运行的前端)将用于发送 `username` 和 `password` 以获取令牌的 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}
🤓 其他版本和变体
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
from typing_extensions import Annotated

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


@app.get("/items/")
async def read_items(token: Annotated[str, Depends(oauth2_scheme)]):
    return {"token": token}

提示

如果可能,请优先使用 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: str = Depends(oauth2_scheme)):
    return {"token": token}

提示

这里 `tokenUrl="token"` 指的是一个我们尚未创建的相对 URL `token`。由于它是相对 URL,因此等同于 `./token`。

因为我们使用的是相对 URL,如果你的 API 位于 `https://example.com/`,那么它将指向 `https://example.com/token`。但如果你的 API 位于 `https://example.com/api/v1/`,那么它将指向 `https://example.com/api/v1/token`。

使用相对 URL 很重要,它可以确保你的应用程序即使在代理后等高级用例中也能正常工作。

此参数不会创建该端点 /*路径操作*,但声明了 URL `/token` 将是客户端用于获取令牌的地址。此信息用于 OpenAPI,然后用于交互式 API 文档系统。

我们很快也将创建实际的路径操作。

信息

如果你是一个非常严格的“Pythonista”,你可能会不喜欢参数名 `tokenUrl` 的这种风格,因为它不是 `token_url`。

这是因为它使用了与 OpenAPI 规范中相同的名称。这样,如果你需要进一步研究任何这些安全方案,可以直接复制粘贴该名称以查找更多信息。

`oauth2_scheme` 变量是 `OAuth2PasswordBearer` 的一个实例,但它也是一个“可调用对象”。

它可以像这样被调用

oauth2_scheme(some, parameters)

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

使用它

现在你可以将 `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}
🤓 其他版本和变体
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
from typing_extensions import Annotated

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


@app.get("/items/")
async def read_items(token: Annotated[str, Depends(oauth2_scheme)]):
    return {"token": token}

提示

如果可能,请优先使用 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: str = Depends(oauth2_scheme)):
    return {"token": token}

这个依赖项将提供一个 `str` 字符串,它会被赋值给 *路径操作函数* 的 `token` 参数。

FastAPI 将知道它可以使用此依赖项在 OpenAPI 方案(和自动 API 文档)中定义“安全方案”。

技术细节

FastAPI 将知道它可以使用 `OAuth2PasswordBearer` 类(在依赖项中声明)来定义 OpenAPI 中的安全方案,因为它继承自 `fastapi.security.oauth2.OAuth2`,而 `fastapi.security.oauth2.OAuth2` 又继承自 `fastapi.security.base.SecurityBase`。

所有与 OpenAPI(和自动 API 文档)集成的安全工具都继承自 `SecurityBase`,这就是 **FastAPI** 如何知道将它们集成到 OpenAPI 中的方式。

它做了什么

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

如果它没有发现 `Authorization` 头部,或者该值不包含 `Bearer` 令牌,它将直接响应 401 状态码错误(`UNAUTHORIZED`)。

你甚至无需检查令牌是否存在就返回错误。你可以确定,如果你的函数被执行,该令牌中将包含一个 `str`。

你可以在交互式文档中尝试一下

我们尚未验证令牌的有效性,但这已经是一个开始了。

总结

所以,只需额外 3 或 4 行代码,你就已经拥有了某种原始形式的安全性。