用Auth0构建和保护FastAPI服务器

367 阅读13分钟

FastAPI是一个相对较新的Python框架,使你能够非常快速地创建应用程序。这个框架允许你用内置模块无缝地读取API请求数据,是Flask的一个轻量级替代品。

在这篇文章中,我们将介绍FastAPI 的特点,设置一个基本的API,使用Auth0保护一个端点,你将了解到它是如何简单地开始的。

在我们开始之前,你也可以通过播放下面的视频来看看这篇博文的视频内容。👇

前提条件

在你开始用FastAPI构建之前,你需要有Python 3.8.2和一个免费的Auth0账户;你可以在这里注册。

如果你安装了那个Python版本和你的Auth0帐户,你就可以创建一个新的FastAPI 应用程序。首先,创建一个新的目录来开发。在这个例子中,你将创建一个目录叫做 fastapi-example和一个名为application 的子文件夹;这个子文件夹是你的代码所在的地方。

在这个 fastapi-example文件夹中,使用以下命令创建一个虚拟环境。

python3 -m venv .env

这创建了一个虚拟环境,它将依赖性与你的计算机库的其他部分分开。换句话说,你不会用库和依赖关系污染全局命名空间,这可能会影响其他的Python项目。

创建完虚拟环境后,你需要激活它。对于基于 Unix 的操作系统,这里有命令。

source .env/bin/activate

如果你是在其他操作系统,你可以在这个文档页面上找到如何激活环境的清单。在激活了你的虚拟环境之后,你可以安装你要使用的软件包。FastAPI,uvicornserver,pyjwt, and updatepip

pip install -U pip
pip install fastapi uvicorn 'pyjwt[crypto]'

开始使用FastAPI

现在所有的库都已经安装完毕,你可以在 文件夹中创建一个 main.py``application 文件;这就是你的API代码所在的地方。该文件的内容 main.py的内容将看起来像这样。

"""main.py
Python FastAPI Auth0 integration example
"""
 
from fastapi import FastAPI
# Creates app instance
app = FastAPI()
 
 
@app.get("/api/public")
def public():
    """No access token required to access this route"""

    result = {
        "status": "success",
        "msg": ("Hello from a public endpoint! You don't need to be "
                "authenticated to see this.")
    }
    return result

让我们把它分解一下。

  • 首先,你要导入FastAPI 库。

  • 然后创建你的应用程序,通过实例化一个 FastAPI()对象来创建你的应用程序。

  • 之后,你用 @app.get来定义一个路由,处理 GET请求

  • 最后,你有一个路径操作函数叫 public()的函数,这是一个每次调用该路径时都会运行的函数,它返回一个带有欢迎信息的字典。

    现在你已经有了你的第一个端点代码,为了让服务器启动和运行,在项目的根目录下运行以下命令。

uvicorn application.main:app --reload

当你的服务器运行时,你可以去到 http://127.0.0.1:8000/docs来查看自动生成的第一个端点的文档,如下图所示。

FastAPI documentation page showing the public endpoint

或者你可以在一个新的终端窗口中通过使用cURL ,提出你的第一个请求。请记住,如果你是一个使用旧版本操作系统的Windows用户,在运行下面的命令之前,你必须先安装curl

curl -X 'GET' \
  --url http://127.0.0.1:8000/api/public

而你应该看到一个JSON作为你刚刚做的请求的结果,与此类似。

{
  "status": "success",
  "msg": "Hello from a public endpoint! You don't need to be authenticated to see this."
}

为了简单起见,你将在本帖的其余部分使用cURL

创建一个私有端点

现在,一个基本的API服务器已经建立,你将在你的 main.py文件。在这个应用程序中,你将有一个 GET /api/public路径,供所有人使用,以及一个 GET /api/private路由,只有你可以用你从Auth0得到的访问令牌访问。

现在你需要更新 main.py文件。下面是你需要对导入部分进行的修改。

  • 首先,你需要从fastapi 模块中导入Depends ,这就是FastAPI的依赖注入系统。
  • 然后,你还需要从模块中导入HTTPBearer 类。 fastapi.security模块中的类,这是一个内置的安全方案,用于带有不记名令牌的授权头文件。
  • 你需要在HTTPBearer 的基础上创建授权方案。这将被用来保证在向私有端点发出的每个请求中都有带有Bearer 令牌的授权头。

该令牌通知API,令牌持有者已被授权访问API,并执行授权时授予的范围所指定的具体行动。

除了更新导入文件外,你还需要实现私有端点。该 /api/private端点也将接受 GET请求,以下是代码的内容 main.py现在看起来是这样的。

"""main.py
Python FastAPI Auth0 integration example
"""
 
from fastapi import Depends, FastAPI  # 👈 new imports
from fastapi.security import HTTPBearer  # 👈 new imports


# Scheme for the Authorization header
token_auth_scheme = HTTPBearer()  # 👈 new code
 
# Creates app instance
app = FastAPI()

@app.get("/api/public")
def public():
    """No access token required to access this route"""
 
    result = {
        "status": "success",
        "msg": ("Hello from a public endpoint! You don't need to be "
                "authenticated to see this.")
    }
    return result


# new code 👇
@app.get("/api/private")
def private(token: str = Depends(token_auth_scheme)):
    """A valid access token is required to access this route"""
 
    result = token.credentials

    return result

Depends 类负责评估一个给定端点收到的针对某个函数、类或实例的每个请求。在这种情况下,它将根据HTTPBearer 方案来评估请求,该方案将检查请求是否有一个带有承载令牌的授权头。

你可以在其文档中找到更多关于FastAPI 依赖性注入工作的细节。

现在你的私有端点会返回收到的令牌。如果没有提供令牌,它将返回一个 403 Forbidden状态代码,并详细说明你是 "Not authenticated".因为你在运行服务器时使用了 --reload标志,你不需要重新运行该命令;uvicorn ,在你每次保存文件时都会接收到这些变化并更新服务器。现在向 GET /api/private端点的请求,以检查其行为。首先,让我们做一个没有传递授权头的请求。

curl -X 'GET' \
  --url 'http://127.0.0.1:8000/api/private'
# {"detail": "Not authenticated"}

现在,如果你用授权头发出一个请求,但用一个随机字符串作为令牌值,你应该看到同样的随机值作为结果。

curl -X 'GET' \
  --url 'http://127.0.0.1:8000/api/private' \
  --header 'Authorization: Bearer FastAPI is awesome'
# "FastAPI is awesome"

正如你所看到的,你的端点并没有受到保护,因为它接受任何字符串作为授权头的值。仅仅收到授权头是不够的,你还必须验证承载令牌的值才能让别人访问端点。让我们来修正这个行为。

设置Auth0一个API

在你准备好在你的端点中验证令牌之前,你需要在Auth0中设置一个API。当这个API被设置好后,你就可以获得Auth0需要的几条信息--受众、客户ID和客户秘密。

你还需要从服务器内部访问该信息;这就是配置文件发挥作用的地方。你将需要在项目的根部创建一个名为 .config的配置文件,在项目的根部。这个文件是这样的 .config文件应该是下面的样子。记住要相应地更新这些值。

# .config
 
[AUTH0]
DOMAIN = your.domain.auth0.com
API_AUDIENCE = your.api.audience
ALGORITHMS = RS256
ISSUER = https://your.domain.auth0.com/

这个配置是在令牌验证阶段检查Auth0配置设置的第一块拼图。另一个需要遵循的好规则是,永远不要把带有环境变量的配置文件提交给源代码。为了防止这种情况的发生,你应该在项目的根目录下创建一个 .gitignore文件,并在项目的根目录下添加 .config文件作为一个条目。

# .gitignore
.config

添加JSON网络令牌(JWT)验证

你的FastAPI 服务器现在有一个 GET /api/private路线,但它还没有被保护。它只检查你在请求中是否有授权头,这意味着你在这个过程中缺少一个步骤:你需要验证访问令牌。要做到这一点,你需要创建一个对象来完成验证JWT的所有步骤,因为Auth0的访问令牌是JWT。

为了将责任从路由定义中分离出来,你应该在 文件夹中创建一个新文件,名为 utils.py``application 的新文件,以保存所有的实用代码,如验证访问令牌和读取配置信息。

首先导入Python的os 库,以及PyJWTconfigparser 库。操作系统库让你可以访问环境变量。JWT 库为你提供了检查和验证 JWT 的功能。来自同名库的ConfigParser 类提供了一种方法,让 Python 读取你先前创建的文件中的配置设置。 .config文件中的配置设置。在导入之后的第一件事是一个叫做 set_up()的函数,你可以在下面看到它。

"""utils.py
"""

import os
import jwt
from configparser import ConfigParser

 
def set_up():
    """Sets up configuration for the app"""

    env = os.getenv("ENV", ".config")

    if env == ".config":
        config = ConfigParser()
        config.read(".config")
        config = config["AUTH0"]
    else:
        config = {
            "DOMAIN": os.getenv("DOMAIN", "your.domain.com"),
            "API_AUDIENCE": os.getenv("API_AUDIENCE", "your.audience.com"),
            "ISSUER": os.getenv("ISSUER", "https://your.domain.com/"),
            "ALGORITHMS": os.getenv("ALGORITHMS", "RS256"),
        }
    return config

set_up()函数负责读取 .config文件并创建一个类似于字典的配置对象。因为这个示例代码也准备在环境变量上运行,所以这个 set_up()函数默认会尝试读取 .config文件。你可以通过将 ENV环境变量为任何其他值,来改变这种行为,在这种情况下,将通过读取所有的环境变量来创建一个字典,你可以看到在上面的 else子句下的所有环境变量来创建字典。

拼图的下一块是神奇发生的地方。你将创建一个VerifyToken 类来处理JWT令牌的验证。

# paste the code 👇 after the set_up() function in the utils.py file
class VerifyToken():
    """Does all the token verification using PyJWT"""

    def __init__(self, token):
        self.token = token
        self.config = set_up()

        # This gets the JWKS from a given URL and does processing so you can
        # use any of the keys available
        jwks_url = f'https://{self.config["DOMAIN"]}/.well-known/jwks.json'
        self.jwks_client = jwt.PyJWKClient(jwks_url)

    def verify(self):
        # This gets the 'kid' from the passed token
        try:
            self.signing_key = self.jwks_client.get_signing_key_from_jwt(
                self.token
            ).key
        except jwt.exceptions.PyJWKClientError as error:
            return {"status": "error", "msg": error.__str__()}
        except jwt.exceptions.DecodeError as error:
            return {"status": "error", "msg": error.__str__()}

        try:
            payload = jwt.decode(
                self.token,
                self.signing_key,
                algorithms=self.config["ALGORITHMS"],
                audience=self.config["API_AUDIENCE"],
                issuer=self.config["ISSUER"],
            )
        except Exception as e:
            return {"status": "error", "message": str(e)}

        return payload

让我们来分解这个类,以了解这里的步骤。

  1. 首先,你有 __init__()方法。
    1. 这个方法负责指定VerifyToken 类需要的token 参数。
    2. 它还运行了 set_up()函数来构建该类所需的配置。
    3. 最后,它设置了 JWKS文件的路径,通过使用PyJWT 包中的PyJWKClient 。一个JSON网络密钥集,简称JWKS,包含验证令牌签名的必要信息,确保它是一个有效的令牌。因为Auth0实现了OAuth 2.0,它有一个 "众所周知 "的端点,你可以调用并获得用于验证令牌及其属性的额外元数据。
  2. 第二,你有 verify()方法。
    1. 这个方法使用密钥ID (kid claim present in the token header),从JWKS中抓取用于验证token签名的密钥。如果这一步因任何可能的错误而失败,将返回错误信息。
    2. 然后,该方法试图通过使用迄今为止收集到的信息来解码JWT。如果出现错误,它会返回错误信息。如果成功,将返回令牌的有效载荷。

让我们看看在前面的章节中如何在我们的私有端点中使用这段代码。

验证一个Auth0访问令牌

最后一块拼图是导入你刚刚在 utils.py文件中创建的类,并将其用于 GET /api/private端点中使用。这里是你需要修改的内容。

  1. 更新导入部分,添加VerifyToken 的导入条款,然后前往你的端点;你还需要导入Response 类和fastapistatus 对象,这样你就可以在出现错误时给出详细的响应。
  2. 然后,你需要调整端点,将令牌传递给VerifyToken 类,并检查该 verify()方法的结果是否是一个错误。

以下是你的 main.py文件在做了上述所有改动后应该是这样的。

"""main.py
Python FastAPI Auth0 integration example
"""
 
from fastapi import Depends, FastAPI, Response, status  # 👈 new imports
from fastapi.security import HTTPBearer
 
from .utils import VerifyToken  # 👈 new import

# Scheme for the Authorization header
token_auth_scheme = HTTPBearer()
 
# Creates app instance
app = FastAPI()
 
 
@app.get("/api/public")
def public():
   """No access token required to access this route"""
 
   result = {
       "status": "success",
       "msg": ("Hello from a public endpoint! You don't need to be "
               "authenticated to see this.")
   }
   return result
 
 
@app.get("/api/private")
def private(response: Response, token: str = Depends(token_auth_scheme)):  # 👈 updated code
    """A valid access token is required to access this route"""
 
    result = VerifyToken(token.credentials).verify()  # 👈 updated code

    # 👇 new code
    if result.get("status"):
       response.status_code = status.HTTP_400_BAD_REQUEST
       return result
    # 👆 new code
 
    return result

有了这个更新,你就正确地设置了你的受保护端点,并为你需要的访问令牌做了所有的验证步骤。🎉

尽管你在启动你的服务器时使用了 --reload标志启动你的服务器,因为你需要确保配置被加载,现在是终止uvicorn 进程然后重新启动服务器的好时机。这将保证你的API能够正常运行,配置参数来自 .config文件中的配置参数或环境变量来保证你的API的正常功能。

在你向FastAPI 服务器中的受保护端点发出请求之前,你需要从Auth0获得访问令牌。你可以从你的API的Test 标签中的Auth0仪表盘上复制它来得到它。

你也可以用curl POST请求到Auth0的 oauth/token端点来获取访问令牌,你可以从Auth0仪表板上的API的Test 标签中复制这个请求。curl请求会是这样的,记得要根据需要填写数值。

curl -X 'POST' \
--url 'https://<YOUR DOMAIN HERE>/oauth/token' \
 --header 'content-type: application/x-www-form-urlencoded' \
 --data grant_type=client_credentials \
 --data 'client_id=<YOUR CLIENT ID HERE>' \
 --data client_secret=<YOUR CLIENT SECRET HERE> \
 --data audience=<YOUR AUDIENCE HERE>

在命令行中,你应该看到一个包含你的承载令牌的响应,像这样。

{
    "access_token": "<YOUR_BEARER_TOKEN>",
    "expires_in": 86400,
    "token_type": "Bearer"
}

现在你可以使用这个访问令牌来访问私有端点。

curl -X 'GET' \
--url 'http://127.0.0.1:8000/api/private' \
--header  'Authorization: Bearer <YOUR_BEARER_TOKEN>'

如果请求成功,服务器将发回访问令牌的有效载荷。

{
    "iss": "https://<YOUR_DOMAIN>/",
    "sub": "iojadoijawdioWDasdijasoid@clients",
    "aud": "http://<YOUR_AUDIENCE>",
    "iat": 1630341660,
    "exp": 1630428060,
    "azp": "ADKASDawdopjaodjwopdAWDdsd",
    "gty": "client-credentials"
}

请记住,如果验证失败,你应该看到出错的细节。

就这样--你已经完成了对私有端点的保护和对其保护的测试。

总结

在这篇博文中你学到了不少东西。首先,你通过实现两个端点--一个是公共端点,一个是私有端点,了解了FastAPI 的基本知识。你看到了向这两个端点发出请求是多么简单。你创建了一个验证类,看到PyJWT是如何帮助你验证Auth0访问令牌的,你还了解了什么是JWKS。

你经历了在Auth0仪表板上创建API的过程。你还学会了如何利用FastAPI提供的依赖注入系统来保护你的一个端点,以帮助你实现集成。而且你很快就完成了这一切。

简而言之,你已经了解了使用FastAPI ,以及如何使用Auth0来保护你的端点是多么容易。

在这个GitHub repo中,你会发现你今天构建的示例应用程序的完整代码。如果你有任何问题,请在本博文的社区论坛主题中提出。