安全 - 第一步

假设后端 API 在某个域。

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

并且,前端要使用后端的 usernamepassword 验证用户身份。

固然,FastAPI 支持 OAuth2 身份验证。

但为了节省开发者的时间,不要只为了查找很少的内容,不得不阅读冗长的规范文档。

我们建议使用 FastAPI 的安全工具。

概览

首先,看看下面的代码是怎么运行的,然后再回过头来了解其背后的原理。

创建 main.py

把下面的示例代码复制到 main.py

Python 3.9+Python 3.8+Python 3.8+ non-Annotated

  1. from typing import Annotated
  2. from fastapi import Depends, FastAPI
  3. from fastapi.security import OAuth2PasswordBearer
  4. app = FastAPI()
  5. oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
  6. @app.get("/items/")
  7. async def read_items(token: Annotated[str, Depends(oauth2_scheme)]):
  8. return {"token": token}
  1. from fastapi import Depends, FastAPI
  2. from fastapi.security import OAuth2PasswordBearer
  3. from typing_extensions import Annotated
  4. app = FastAPI()
  5. oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
  6. @app.get("/items/")
  7. async def read_items(token: Annotated[str, Depends(oauth2_scheme)]):
  8. return {"token": token}

Tip

尽可能选择使用 Annotated 的版本。

  1. from fastapi import Depends, FastAPI
  2. from fastapi.security import OAuth2PasswordBearer
  3. app = FastAPI()
  4. oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
  5. @app.get("/items/")
  6. async def read_items(token: str = Depends(oauth2_scheme)):
  7. return {"token": token}

运行

说明

先安装 python-multipart

安装命令: pip install python-multipart

这是因为 OAuth2 使用表单数据发送 usernamepassword

用下面的命令运行该示例:

  1. $ uvicorn main:app --reload
  2. <span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

查看文档

打开 API 文档: http://127.0.0.1:8000/docs。

界面如下图所示:

安全 - 第一步 - 图1

Authorize 按钮!

页面右上角出现了一个「Authorize」按钮。

路径操作的右上角也出现了一个可以点击的小锁图标。

点击 Authorize 按钮,弹出授权表单,输入 usernamepassword 及其它可选字段:

安全 - 第一步 - 图2

笔记

目前,在表单中输入内容不会有任何反应,后文会介绍相关内容。

虽然此文档不是给前端最终用户使用的,但这个自动工具非常实用,可在文档中与所有 API 交互。

前端团队(可能就是开发者本人)可以使用本工具。

第三方应用与系统也可以调用本工具。

开发者也可以用它来调试、检查、测试应用。

密码流

现在,我们回过头来介绍这段代码的原理。

Password 是 OAuth2 定义的,用于处理安全与身份验证的方式()。

OAuth2 的设计目标是为了让后端或 API 独立于服务器验证用户身份。

但在本例中,FastAPI 应用会处理 API 与身份验证。

下面,我们来看一下简化的运行流程:

  • 用户在前端输入 usernamepassword,并点击回车
  • (用户浏览器中运行的)前端把 usernamepassword 发送至 API 中指定的 URL(使用 tokenUrl="token" 声明)
  • API 检查 usernamepassword,并用令牌(Token) 响应(暂未实现此功能):
  • 令牌只是用于验证用户的字符串
  • 一般来说,令牌会在一段时间后过期
    • 过时后,用户要再次登录
    • 这样一来,就算令牌被人窃取,风险也较低。因为它与永久密钥不同,在绝大多数情况下不会长期有效
  • 前端临时将令牌存储在某个位置
  • 用户点击前端,前往前端应用的其它部件
  • 前端需要从 API 中提取更多数据:
    • 为指定的端点(Endpoint)进行身份验证
    • 因此,用 API 验证身份时,要发送值为 Bearer + 令牌的请求头 Authorization
    • 假如令牌为 foobarAuthorization 请求头就是: Bearer foobar

FastAPIOAuth2PasswordBearer

FastAPI 提供了不同抽象级别的安全工具。

本例使用 OAuth2Password 流以及 Bearer 令牌(Token)。为此要使用 OAuth2PasswordBearer 类。

说明

Bearer 令牌不是唯一的选择。

但它是最适合这个用例的方案。

甚至可以说,它是适用于绝大多数用例的最佳方案,除非您是 OAuth2 的专家,知道为什么其它方案更合适。

本例中,FastAPI 还提供了构建工具。

创建 OAuth2PasswordBearer 的类实例时,要传递 tokenUrl 参数。该参数包含客户端(用户浏览器中运行的前端) 的 URL,用于发送 usernamepassword,并获取令牌。

  1. from fastapi import Depends, FastAPI
  2. from fastapi.security import OAuth2PasswordBearer
  3. app = FastAPI()
  4. oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
  5. @app.get("/items/")
  6. async def read_items(token: str = Depends(oauth2_scheme)):
  7. 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 的实例,也是可调用项

以如下方式调用:

  1. oauth2_scheme(some, parameters)

因此,Depends 可以调用 oauth2_scheme 变量。

使用

接下来,使用 Dependsoauth2_scheme 传入依赖项。

  1. from fastapi import Depends, FastAPI
  2. from fastapi.security import OAuth2PasswordBearer
  3. app = FastAPI()
  4. oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
  5. @app.get("/items/")
  6. async def read_items(token: str = Depends(oauth2_scheme)):
  7. return {"token": token}

该依赖项使用字符串(str)接收路径操作函数的参数 token

FastAPI 使用依赖项在 OpenAPI 概图(及 API 文档)中定义安全方案

技术细节

FastAPI 使用(在依赖项中声明的)类 OAuth2PasswordBearer 在 OpenAPI 中定义安全方案,这是因为它继承自 fastapi.security.oauth2.OAuth2,而该类又是继承自fastapi.security.base.SecurityBase

所有与 OpenAPI(及 API 文档)集成的安全工具都继承自 SecurityBase, 这就是为什么 FastAPI 能把它们集成至 OpenAPI 的原因。

实现的操作

FastAPI 校验请求中的 Authorization 请求头,核对请求头的值是不是由 Bearer + 令牌组成, 并返回令牌字符串(str)。

如果没有找到 Authorization 请求头,或请求头的值不是 Bearer + 令牌。FastAPI 直接返回 401 错误状态码(UNAUTHORIZED)。

开发者不需要检查错误信息,查看令牌是否存在,只要该函数能够执行,函数中就会包含令牌字符串。

正如下图所示,API 文档已经包含了这项功能:

安全 - 第一步 - 图3

目前,暂时还没有实现验证令牌是否有效的功能,不过后文很快就会介绍的。

小结

看到了吧,只要多写三四行代码,就可以添加基础的安全表单。