跳到内容

Security - First Steps

让我们设想一下,您的后端 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 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“流”是 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 包含 foobarAuthorization 头部的内容将是:Bearer foobar

FastAPIOAuth2PasswordBearer

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

在本例中,我们将使用 OAuth2,配合 Password 流,使用 Bearer token。我们通过 OAuth2PasswordBearer 类来实现。

信息

“Bearer” token 并不是唯一的选项。

但对于我们的用例来说,它是最好的。

而且它可能适用于大多数用例,除非您是 OAuth2 专家,并且确切地知道为什么有其他选项更适合您的需求。

在这种情况下,FastAPI 也提供了工具供您构建。

当我们创建一个 OAuth2PasswordBearer 类的实例时,我们会传入 tokenUrl 参数。此参数包含客户端(用户浏览器中运行的前端)将用于发送 usernamepassword 以获取 token 的 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 对于确保您的应用程序即使在代理后面等高级用例中也能正常工作很重要。

此参数不会创建该端点/路径操作,但它声明 URL /token 将是客户端应用来获取 token 的 URL。该信息用于 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}
🤓 其他版本和变体

提示

如果可能,请优先使用 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 schema(和自动 API 文档)中的“安全方案”。

技术细节

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

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

它的作用

它将查看请求中的 Authorization 头部,检查其值是否为 Bearer 加上某个 token,然后将 token 作为 str 返回。

如果它没有看到 Authorization 头部,或者其值不是 Bearer token,它将直接响应 401 状态码错误(UNAUTHORIZED)。

您甚至不必检查 token 是否存在即可返回错误。您可以确定,如果您的函数被执行,那么该 token 将是一个 str

您已经在交互式文档中尝试过了:

我们还没有验证 token 的有效性,但这已经是一个开始了。

总结

因此,只需 3 或 4 行额外的代码,您就已经拥有了一些基本的安全形式。