安全性 - 初步¶
假设你的 **后端** 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 行代码,你就已经拥有了某种原始形式的安全性。