跳转至

Security - First Steps

假设你的后端 API 部署在某个域名下。

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

你希望前端能够通过用户名密码与后端进行认证。

我们可以使用 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
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

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}

运行程序

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

你将看到如下界面:

Authorize 按钮!

你已经有了一个崭新的 "Authorize" 按钮。

而且你的路径操作右上角会出现一个小锁图标,点击即可进行认证。

点击后会出现一个认证表单,用于输入 usernamepassword(以及其他可选字段):

Note

目前无论你在表单中输入什么内容都不会生效,但我们后续会实现功能。

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

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

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

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

password 流程

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

password "流程"是 OAuth2 中定义的一种方式("flows"),用于处理安全和认证。

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

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}

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 对于确保你的应用程序在高级用例(如代理后面)中仍然正常工作非常重要。

这个参数不会创建该端点/路径操作,而是声明 URL /token 是客户端应该用于获取 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}
🤓 Other versions and variants
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

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 加上某个 token,并将 token 作为 str 返回。

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

你甚至不需要检查 token 是否存在以返回错误。你可以确信,如果你的函数被执行,它将在该 token 中有一个 str

你可以在交互式文档中立即尝试:

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

回顾

所以,仅仅增加了 3 到 4 行代码,你就已经拥有了一些初步的安全形式。