跳到内容

代理后面

在许多情况下,您会在 FastAPI 应用前面使用 代理,例如 Traefik 或 Nginx。

这些代理可以处理 HTTPS 证书和其他事务。

代理转发头

应用前面的代理通常会在将请求发送到服务器之前即时设置一些头信息,以便服务器知道请求是由代理转发的,并告知其原始(公共)URL,包括域名,以及它正在使用 HTTPS 等。

服务器程序(例如通过 FastAPI CLIUvicorn)能够解析这些头信息,然后将该信息传递给您的应用。

但出于安全原因,由于服务器不知道它是否位于受信任的代理后面,因此它不会解析这些头信息。

启用代理转发头

您可以使用 CLI 选项 --forwarded-allow-ips 启动 FastAPI CLI,并传递应信任以读取这些转发头的 IP 地址。

如果将其设置为 --forwarded-allow-ips="*",它将信任所有传入的 IP。

如果您的服务器位于受信任的代理后面,并且只有代理与它通信,这将使其接受该代理的任何 IP。

$ fastapi run --forwarded-allow-ips="*"

<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

HTTPS 重定向

例如,假设您定义了一个路径操作 /items/

from fastapi import FastAPI

app = FastAPI()


@app.get("/items/")
def read_items():
    return ["plumbus", "portal gun"]

如果客户端尝试访问 /items,默认情况下,它将被重定向到 /items/

但在设置CLI 选项 --forwarded-allow-ips 之前,它可能会重定向到 https://:8000/items/

但也许您的应用程序托管在 https://mysuperapp.com,重定向应该指向 https://mysuperapp.com/items/

通过设置 --proxy-headers,FastAPI 现在将能够重定向到正确的位置。😎

https://mysuperapp.com/items/

提示

如果您想了解更多关于 HTTPS 的信息,请查看指南 关于 HTTPS

代理转发头如何工作

以下是代理在客户端和应用服务器之间添加转发头信息的直观表示

sequenceDiagram
    participant Client
    participant Proxy as Proxy/Load Balancer
    participant Server as FastAPI Server

    Client->>Proxy: HTTPS Request<br/>Host: mysuperapp.com<br/>Path: /items

    Note over Proxy: Proxy adds forwarded headers

    Proxy->>Server: HTTP Request<br/>X-Forwarded-For: [client IP]<br/>X-Forwarded-Proto: https<br/>X-Forwarded-Host: mysuperapp.com<br/>Path: /items

    Note over Server: Server interprets headers<br/>(if --forwarded-allow-ips is set)

    Server->>Proxy: HTTP Response<br/>with correct HTTPS URLs

    Proxy->>Client: HTTPS Response

代理会拦截原始客户端请求,并在将请求传递给应用服务器之前添加特殊的转发头(X-Forwarded-*)。

这些头信息保留了原始请求的信息,否则这些信息可能会丢失

  • X-Forwarded-For:原始客户端的 IP 地址
  • X-Forwarded-Proto:原始协议(https
  • X-Forwarded-Host:原始主机(mysuperapp.com

FastAPI CLI 配置了 --forwarded-allow-ips 时,它会信任这些头信息并使用它们,例如生成正确的重定向 URL。

带有被剥离的路径前缀的代理

您可以让代理为您的应用程序添加路径前缀。

在这些情况下,您可以使用 root_path 来配置您的应用程序。

root_path 是 ASGI 规范(FastAPI 在 Starlette 的基础上构建)提供的一种机制。

root_path 用于处理这些特定情况。

它也在内部用于挂载子应用程序。

在这种情况下,具有被剥离路径前缀的代理意味着您可以在代码中声明一个路径为 /app,然后您会添加一个额外的层(代理),将您的FastAPI应用程序放在像 /api/v1 这样的路径下。

在这种情况下,原始路径 /app 实际上将在 /api/v1/app 提供。

即使您的所有代码都假定只有一个 /app

from fastapi import FastAPI, Request

app = FastAPI()


@app.get("/app")
def read_main(request: Request):
    return {"message": "Hello World", "root_path": request.scope.get("root_path")}

代理会在将请求传输到应用服务器(可能是通过 FastAPI CLI 的 Uvicorn)之前,即时“剥离”路径前缀,让您的应用程序认为它在 /app 下提供服务,这样您就不必更新所有代码以包含前缀 /api/v1

到目前为止,一切都会正常工作。

但是,当您打开集成的文档 UI(前端)时,它会期望在 /openapi.json 处获取 OpenAPI 模式,而不是 /api/v1/openapi.json

因此,前端(在浏览器中运行)将尝试访问 /openapi.json,但无法获取 OpenAPI 模式。

因为我们的应用程序有一个路径前缀为 /api/v1 的代理,前端需要在 /api/v1/openapi.json 处获取 OpenAPI 模式。

graph LR

browser("Browser")
proxy["Proxy on http://0.0.0.0:9999/api/v1/app"]
server["Server on http://127.0.0.1:8000/app"]

browser --> proxy
proxy --> server

提示

IP 地址 0.0.0.0 通常用于表示程序侦听该机器/服务器上所有可用的 IP 地址。

文档 UI 也需要 OpenAPI 模式来声明此 API 服务器位于 /api/v1(代理后面)。例如

{
    "openapi": "3.1.0",
    // More stuff here
    "servers": [
        {
            "url": "/api/v1"
        }
    ],
    "paths": {
            // More stuff here
    }
}

在此示例中,“代理”可以是Traefik之类的东西。服务器可以是 FastAPI CLI 结合Uvicorn运行您的 FastAPI 应用程序。

提供 root_path

要实现这一点,您可以使用命令行选项 --root-path,例如

$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1

<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

如果您使用 Hypercorn,它也有 --root-path 选项。

技术细节

ASGI 规范为此用例定义了 root_path

--root-path 命令行选项提供了该 root_path

检查当前的 root_path

您可以在每次请求时获取应用程序当前使用的 root_path,它是 scope 字典的一部分(这是 ASGI 规范的一部分)。

此处我们将其包含在消息中仅用于演示目的。

from fastapi import FastAPI, Request

app = FastAPI()


@app.get("/app")
def read_main(request: Request):
    return {"message": "Hello World", "root_path": request.scope.get("root_path")}

然后,如果您使用以下命令启动 Uvicorn

$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1

<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

响应可能如下所示

{
    "message": "Hello World",
    "root_path": "/api/v1"
}

在 FastAPI 应用中设置 root_path

或者,如果您无法提供命令行选项(如 --root-path 或等效选项),您可以在创建 FastAPI 应用时设置 root_path 参数

from fastapi import FastAPI, Request

app = FastAPI(root_path="/api/v1")


@app.get("/app")
def read_main(request: Request):
    return {"message": "Hello World", "root_path": request.scope.get("root_path")}

root_path 传递给 FastAPI 相当于将 --root-path 命令行选项传递给 Uvicorn 或 Hypercorn。

关于 root_path

请记住,服务器(Uvicorn)不会使用该 root_path 做任何其他事情,除了将其传递给应用程序。

但是,如果您通过浏览器访问 http://127.0.0.1:8000/app,您将看到正常响应

{
    "message": "Hello World",
    "root_path": "/api/v1"
}

因此,它不会期望在 http://127.0.0.1:8000/api/v1/app 访问。

Uvicorn 将期望代理在 http://127.0.0.1:8000/app 处访问 Uvicorn,然后由代理负责在其之上添加额外的 /api/v1 前缀。

关于带有被剥离的路径前缀的代理

请记住,带有被剥离路径前缀的代理只是配置方式之一。

在许多情况下,默认设置可能是代理没有被剥离的路径前缀。

在这种情况下(没有被剥离的路径前缀),代理将侦听类似 https://myawesomeapp.com 的地址,然后如果浏览器访问 https://myawesomeapp.com/api/v1/app,并且您的服务器(例如 Uvicorn)侦听 http://127.0.0.1:8000,代理(没有被剥离的路径前缀)将以相同的路径访问 Uvicorn:http://127.0.0.1:8000/api/v1/app

使用 Traefik 本地测试

您可以使用 Traefik 轻松地在本地进行带有被剥离路径前缀的实验。

下载 Traefik,它是一个单一的二进制文件,您可以解压缩文件并直接从终端运行它。

然后创建一个名为 traefik.toml 的文件,内容如下

[entryPoints]
  [entryPoints.http]
    address = ":9999"

[providers]
  [providers.file]
    filename = "routes.toml"

这告诉 Traefik 侦听端口 9999,并使用另一个名为 routes.toml 的文件。

提示

我们使用端口 9999 而不是标准的 HTTP 端口 80,这样您就不需要使用管理员(sudo)权限来运行它。

现在创建另一个名为 routes.toml 的文件

[http]
  [http.middlewares]

    [http.middlewares.api-stripprefix.stripPrefix]
      prefixes = ["/api/v1"]

  [http.routers]

    [http.routers.app-http]
      entryPoints = ["http"]
      service = "app"
      rule = "PathPrefix(`/api/v1`)"
      middlewares = ["api-stripprefix"]

  [http.services]

    [http.services.app]
      [http.services.app.loadBalancer]
        [[http.services.app.loadBalancer.servers]]
          url = "http://127.0.0.1:8000"

此文件配置 Traefik 使用路径前缀 /api/v1

然后 Traefik 会将请求重定向到运行在 http://127.0.0.1:8000 上的 Uvicorn。

现在启动 Traefik

$ ./traefik --configFile=traefik.toml

INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml

然后启动您的应用,使用 --root-path 选项

$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1

<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

检查响应

现在,如果您访问 Uvicorn 端口的 URL:http://127.0.0.1:8000/app,您将看到正常响应

{
    "message": "Hello World",
    "root_path": "/api/v1"
}

提示

请注意,尽管您通过 http://127.0.0.1:8000/app 访问它,但它显示了 root_path/api/v1,这是从选项 --root-path 获取的。

现在打开 Traefik 端口的 URL,包括路径前缀:http://127.0.0.1:9999/api/v1/app

我们得到了相同的响应

{
    "message": "Hello World",
    "root_path": "/api/v1"
}

但这次是在带有代理提供的路径前缀的 URL 上:/api/v1

当然,这里的想法是每个人都会通过代理访问应用程序,所以带有路径前缀 /api/v1 的版本是“正确”的版本。

而没有路径前缀的版本(由 Uvicorn 直接提供的 http://127.0.0.1:8000/app),将仅供代理(Traefik)访问。

这演示了代理(Traefik)如何使用路径前缀,以及服务器(Uvicorn)如何使用 --root-path 选项中的 root_path

检查文档 UI

但这里有趣的部分来了。✨

访问应用程序的“官方”方式是通过带有我们定义的路径前缀的代理。因此,正如我们所料,如果您尝试直接访问 Uvicorn 提供的文档 UI,而 URL 中没有路径前缀,它将不起作用,因为它期望通过代理访问。

您可以在 http://127.0.0.1:8000/docs 处查看

但是,如果我们通过代理的“官方”URL(使用端口 9999)访问文档 UI,在 /api/v1/docs 处,它就能正常工作!🎉

您可以在 http://127.0.0.1:9999/api/v1/docs 处查看

正如我们所期望的那样。✔️

这是因为 FastAPI 使用此 root_path 来创建 OpenAPI 中的默认 server,其 URL 由 root_path 提供。

附加服务器

警告

这是一个更高级的用例。您可以随意跳过。

默认情况下,FastAPI 会在 OpenAPI 模式中创建一个 server,其 URL 为 root_path

但是您也可以提供其他备用的 servers,例如,如果您希望同一个文档 UI 与暂存环境和生产环境进行交互。

如果您传递一个自定义的 servers 列表,并且存在 root_path(因为您的 API 位于代理后面),FastAPI 会将一个带有此 root_path 的“server”插入到列表的开头。

例如

from fastapi import FastAPI, Request

app = FastAPI(
    servers=[
        {"url": "https://stag.example.com", "description": "Staging environment"},
        {"url": "https://prod.example.com", "description": "Production environment"},
    ],
    root_path="/api/v1",
)


@app.get("/app")
def read_main(request: Request):
    return {"message": "Hello World", "root_path": request.scope.get("root_path")}

将生成一个像这样的 OpenAPI 模式

{
    "openapi": "3.1.0",
    // More stuff here
    "servers": [
        {
            "url": "/api/v1"
        },
        {
            "url": "https://stag.example.com",
            "description": "Staging environment"
        },
        {
            "url": "https://prod.example.com",
            "description": "Production environment"
        }
    ],
    "paths": {
            // More stuff here
    }
}

提示

注意自动生成的服务器,其 url 值为 /api/v1,这是从 root_path 获取的。

http://127.0.0.1:9999/api/v1/docs 的文档 UI 中,它看起来会像

提示

文档 UI 将与您选择的服务器进行交互。

技术细节

OpenAPI 规范中的 servers 属性是可选的。

如果您不指定 servers 参数且 root_path 等于 /,则默认情况下,生成的 OpenAPI 模式中的 servers 属性将被完全省略,这等同于一个 url 值为 / 的单个服务器。

禁用来自 root_path 的自动服务器

如果您不希望 FastAPI 使用 root_path 包含一个自动服务器,您可以使用参数 root_path_in_servers=False

from fastapi import FastAPI, Request

app = FastAPI(
    servers=[
        {"url": "https://stag.example.com", "description": "Staging environment"},
        {"url": "https://prod.example.com", "description": "Production environment"},
    ],
    root_path="/api/v1",
    root_path_in_servers=False,
)


@app.get("/app")
def read_main(request: Request):
    return {"message": "Hello World", "root_path": request.scope.get("root_path")}

然后它将不会包含在 OpenAPI 模式中。

挂载子应用

如果您需要挂载子应用程序(如 子应用程序 - 挂载 中所述),同时使用带有 root_path 的代理,您可以像预期的那样正常进行。

FastAPI 将在内部智能地使用 root_path,因此它会正常工作。✨