Skip to content

安全 - 第一步

让我们想象一下,你的**后端** 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}

Tip

如果可能的话,建议使用 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}

运行它

Info

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

然而,如果你使用 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

你会看到类似这样的内容:

"授权按钮!"

你已经拥有了一个闪亮的新“授权”按钮。

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

如果你点击它,你会看到一个小的授权表单,用于输入 usernamepassword(以及其他可选字段):

Note

无论你在表单中输入什么,目前都不会生效。但我们很快就会实现这一点。

当然,这并不是最终用户的完整前端,但它是一个很好的自动工具,可以交互式地记录你的整个 API。

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

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

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

password 流程

现在让我们回过头来,理解这一切是什么。

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

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

但在这种情况下,同一个 FastAPI 应用程序将处理 API 和身份验证。

因此,让我们从简化的角度来回顾一下:

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

FastAPIOAuth2PasswordBearer

FastAPI 提供了多种工具,在不同抽象层次上实现这些安全特性。

在这个示例中,我们将使用 OAuth2,通过 Password 流程,使用 Bearer token。我们通过使用 OAuth2PasswordBearer 类来实现这一点。

Info

"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}
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}

Tip

如果可能的话,建议使用 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}

Tip

这里的 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 这样的高级用例中也能正常工作。

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

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

Info

如果你是一个非常严格的 "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}
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}

Tip

如果可能的话,建议使用 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 行代码,你已经有了某种原始形式的安全性。