安全 - 第一步¶
让我们想象一下,你的**后端** 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 使用“表单数据”来发送 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。
你会看到类似这样的内容:
"授权按钮!"
你已经拥有了一个闪亮的新“授权”按钮。
你的*路径操作*右上角有一个小锁,你可以点击它。
如果你点击它,你会看到一个小的授权表单,用于输入 username
和 password
(以及其他可选字段):
Note
无论你在表单中输入什么,目前都不会生效。但我们很快就会实现这一点。
当然,这并不是最终用户的完整前端,但它是一个很好的自动工具,可以交互式地记录你的整个 API。
它可以被前端团队使用(也可以是你自己)。
它可以被第三方应用程序和系统使用。
你也可以用它来调试、检查和测试同一个应用程序。
password
流程¶
现在让我们回过头来,理解这一切是什么。
password
“流程”是 OAuth2 中定义的一种方式(“流程”),用于处理安全和身份验证。
OAuth2 的设计使得后端或 API 可以独立于用户认证的服务器。
但在这种情况下,同一个 FastAPI 应用程序将处理 API 和身份验证。
因此,让我们从简化的角度来回顾一下:
- 用户在前端输入
username
和password
,并按下Enter
。 - 前端(运行在用户的浏览器中)将
username
和password
发送到我们 API 中的一个特定 URL(通过tokenUrl="token"
声明)。 - API 检查
username
和password
,并返回一个“token”(我们还没有实现任何这些功能)。- “token”只是一个包含一些内容的字符串,我们稍后可以使用它来验证这个用户。
- 通常,token 会在一段时间后过期。
- 因此,用户需要在某个时间点再次登录。
- 如果 token 被盗,风险较小。它不像一个永久密钥,会永远有效(在大多数情况下)。
- 前端将该 token 临时存储在某个地方。
- 用户点击前端中的某个部分,进入前端 Web 应用的另一个部分。
- 前端需要从 API 获取更多数据。
- 但它需要对该特定端点进行身份验证。
- 因此,为了使用我们的 API 进行身份验证,它发送一个
Authorization
头,其值为Bearer
加上 token。 - 如果 token 包含
foobar
,则Authorization
头的内容将是:Bearer foobar
。
FastAPI 的 OAuth2PasswordBearer
¶
FastAPI 提供了多种工具,在不同抽象层次上实现这些安全特性。
在这个示例中,我们将使用 OAuth2,通过 Password 流程,使用 Bearer token。我们通过使用 OAuth2PasswordBearer
类来实现这一点。
Info
"bearer" 令牌并不是唯一的选择。
但它最适合我们的使用场景。
对于大多数使用场景来说,它可能也是最佳选择,除非你是 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}
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 行代码,你已经有了某种原始形式的安全性。