跳到内容

安全性 - 第一步

设想你在某个域名下拥有一个后端 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}
🤓 其他版本和变体

提示

如果可能,请优先使用 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 使用“表单数据”来发送 usernamepassword

运行示例:

$ fastapi dev

<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

你会看到类似这样的内容

授权按钮!

你现在拥有了一个崭新的“授权”按钮。

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

点击它,会出现一个小的授权表单,用于输入 usernamepassword(以及其他可选字段)。

注意

在表单中输入任何内容都没用,因为还没实现后续逻辑。但我们会讲到那一步。

这当然不是最终用户的“前端”,但它是一个极好的自动化工具,可以交互式地记录你的所有 API。

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

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

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

password 流程

现在让我们退一步,理解一下这一切都是什么。

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

OAuth2 的设计初衷是让后端或 API 可以独立于对用户进行身份验证的服务器。

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

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

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

FastAPIOAuth2PasswordBearer

FastAPI 在不同的抽象层级上提供了多种工具来实现这些安全功能。

在本例中,我们将使用 OAuth2,配合 Password 流程和 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}
🤓 其他版本和变体

提示

如果可能,请优先使用 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 很重要,这可以确保你的应用程序即使在代理后 (Behind a Proxy) 这样的高级用例中也能正常工作。

此参数不会自动创建该端点 / 路径操作,但它声明了 /token URL 是客户端应该用来获取令牌的地址。该信息在 OpenAPI 以及随后的交互式 API 文档系统中会被使用。

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

信息

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

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

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

它可以像这样被调用:

oauth2_scheme(some, parameters)

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

使用它

现在你可以通过 Depends 将该 oauth2_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}
🤓 其他版本和变体

提示

如果可能,请优先使用 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.base.SecurityBase

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

它的作用

它会去请求中查找那个 Authorization 请求头,检查值是否为 Bearer 加上某个令牌,并以 str 的形式返回该令牌。

如果它没发现 Authorization 请求头,或者值中没有 Bearer 令牌,它会直接返回一个 401 状态码错误(UNAUTHORIZED)。

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

你现在就可以在交互式文档中进行尝试了。

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

总结

因此,只需多写 3 到 4 行代码,你就已经拥有了某种原始形式的安全性。