博客 | NGINX

将 NGINX 部署为 API 网关,第 1 部分

NGINX-F5-horiz-black-type-RGB 的一部分
Liam Crilly 缩略图
利亚姆·克里利
2021 年 1 月 20 日发布

这是我们关于部署 NGINX 开源和 NGINX Plus 作为 API 网关的系列博文中的第一篇:

  • 这篇文章提供了几种用例的详细配置说明。 它最初于 2018 年发布,现已更新以反映 API 配置的当前最佳实践,使用嵌套位置块来路由请求,而不是重写规则。
  • 第 2 部分扩展了这些用例,并研究了可用于保护和确保生产中的后端 API 服务的一系列保护措施。
  • 第 3 部分介绍如何将 NGINX 开源和 NGINX Plus 部署为 gRPC 服务的 API 网关。

笔记: 除非另有说明,本文中的所有信息都适用于 NGINX Open Source 和 NGINX Plus。 为了方便阅读,本博客的其余部分简称为“NGINX”。

现代应用架构的核心是 HTTP API。HTTP 使应用能够快速构建并轻松维护。 HTTP API 提供了一个通用接口,无论应用的规模如何,无论是从单一用途的微服务还是包罗万象的整体。 通过使用 HTTP,支持超大规模互联网属性的 Web应用交付方面的进步也可用于提供可靠且高性能的 API 交付。

有关 API 网关对于微服务应用的重要性的出色介绍,请参阅构建微服务: 使用 API 网关 在我们的博客上。

作为领先的高性能、轻量级反向代理和负载均衡器,NGINX 具有处理 API 流量所需的高级 HTTP 处理功能。 这使得 NGINX 成为构建 API 网关的理想平台。 在这篇博文中,我们描述了许多常见的 API 网关用例,并展示了如何配置 NGINX 以高效、可扩展且易于维护的方式处理它们。 我们描述了完整的配置,它可以作为生产部署的基础。

介绍 Warehouse API

API 网关的主要功能是为多个 API 提供单一、一致的入口点,无论它们在后端如何实现或部署。 并非所有API都是微服务应用。 我们的 API 网关需要管理现有的 API、整体式应用程序和正在部分过渡到微服务的应用。

在这篇博文中,我们提到了一个用于库存管理的假设 API,即“仓库 API”。 我们使用示例配置代码来说明不同的用例。 Warehouse API 是一个 RESTful API,它使用 JSON 请求并生成 JSON 响应。 但是,当部署为 API 网关时,使用 JSON 并不是 NGINX 的限制或要求;NGINX 与 API 本身使用的架构风格和数据格式无关。

Warehouse API 是作为一组离散微服务实现的,并作为单个 API 发布。库存和定价资源是作为单独的服务实现的,并部署到不同的后端。 因此API的路径结构是:    

api
└── 仓库
═── 库存
└── 定价

例如,为了查询当前仓库库存,客户端应用向/api/warehouse/inventory发出 HTTP GET请求。

多应用API网关架构

组织 NGINX 配置

使用 NGINX 作为 API 网关的一个优点是,它可以执行该角色,同时充当现有 HTTP 流量的反向代理、负载均衡器和 Web 服务器。 如果 NGINX 已经是您的应用交付堆栈的一部分,那么通常不需要部署单独的 API 网关。 但是,API 网关的一些默认行为与基于浏览器的流量的预期行为不同。 因此,我们将 API 网关配置与任何现有(或未来)的基于浏览器的流量配置分开。

为了实现这种分离,我们创建了一个支持多用途 NGINX 实例的配置布局,并提供了一个通过 CI/CD 管道自动执行配置部署的便捷结构。 /etc/nginx下的最终目录结构如下所示。

etc/
└── nginx/
═── api_conf.d/ ………………………………… 每个 API 的配置子目录
│ └── warehouse_api.conf …… Warehouse API 的定义和策略
═── api_backends.conf ………………… 后端服务(上游)
═── api_gateway.conf …………………… API 网关服务器的顶级配置
═── api_json_errors.conf ………… JSON 格式的 HTTP 错误响应
═── conf.d/
│ ═── ...
│ └──existing_apps.conf
└── nginx.conf

所有 API 网关配置的目录和文件名都以api_为前缀。 每个文件和目录都支持 API 网关的不同特性或功能,如下所述。 warehouse_api.conf文件是下面讨论的配置文件的通用替代品,以不同的方式定义 Warehouse API。

定义顶级 API 网关

所有 NGINX 配置都从主配置文件nginx.conf开始。 为了读取 API 网关配置,我们在nginx.conf中的http块中添加一个include指令,该指令引用包含网关配置的文件api_gateway.conf (就在下面的第 28 行)。 请注意,默认的nginx.conf文件使用include指令从conf.d子目录 (第 29 行) 中提取基于浏览器的 HTTP 配置。 这篇博文大量使用了include指令来提高可读性并实现某些部分配置的自动化。

 

api_gateway.conf文件定义将 NGINX 作为 API 网关公开给客户端的虚拟服务器。 此配置在单个入口点https://api.example.com/ (第 9 行)公开 API 网关发布的所有 API,并按照第 12 行至第 17 行的配置受到 TLS 保护。 请注意,此配置纯是 HTTPS – 没有纯文本 HTTP 侦听器。 我们希望 API 客户端知道正确的入口点并默认建立 HTTPS 连接。

此配置旨在静态 - 各个 API 及其后端服务的详细信息在第 20 行的include指令引用的文件中指定。 第 23 行到第 26 行涉及错误处理,在下面的响应错误中讨论。

 

单一服务与…… 微服务 API 后端

某些 API 可能在单个后端实现,但出于弹性或负载均衡的原因,我们通常预计会有多个后端。 使用微服务 API,我们为每项服务定义单独的后端;它们共同构成完整的 API。在这里,我们的 Warehouse API 部署为两个独立的服务,每个服务都有多个后端。

 

API 网关发布的所有 API 的所有后端 API 服务均在api_backends.conf中定义。 这里我们在每个上游块中使用多个 IP 地址-端口对来指示 API 代码的部署位置,但也可以使用主机名。 NGINX Plus 订阅者还可以利用动态DNS 负载均衡将新的后端自动添加到运行时配置中。

定义仓库 API

仓库 API 由嵌套配置中的多个位置块定义,如以下示例所示。 外部位置块( /api/warehouse )标识基本路径,嵌套位置在该路径下指定路由到后端 API 服务的有效 URI。 使用外部块使我们能够定义适用于整个 API 的通用策略(在此示例中为第 6 行的日志配置)。

 

NGINX 具有一个高度高效且灵活的系统,用于将请求 URI 与配置的一部分进行匹配。 位置指令的顺序并不重要——选择最具体的匹配。 这里,第 10 行和第 14 行的嵌套位置定义了两个比外部位置块更具体的 URI;每个嵌套块中的proxy_pass指令将请求路由到适当的上游组。 除非需要为某些 URI 提供更具体的策略,否则策略配置从外部位置继承。

任何与嵌套位置不匹配的 URI 都由外部位置处理,其中包括一个 catch‑all 指令(第 18 行),该指令返回响应 404 (不是 成立) 对于所有无效的 URI。

选择广泛还是…… API的精确定义

API 定义有两种方法——广泛和精确。 每个 API 最合适的方法取决于 API 的安全要求以及后端服务是否希望处理无效 URI。

在上面的warehouse_api_simple.conf中,我们对 Warehouse API 使用了广泛的方法,在第 10 行和第 14 行定义 URI 前缀,以便以其中一个前缀开头的 URI 被代理到适当的后端服务。 通过这种广泛的基于前缀的位置匹配,对以下 URI 的 API 请求均有效:

/api/仓库/库存
/api/仓库/库存/
/api/仓库/库存/foo
/api/仓库/inventoryfoo
/api/仓库/inventoryfoo/bar/

如果唯一的考虑是将每个请求代理到正确的后端服务,则广泛的方法可以提供最快的处理和最紧凑的配置。 另一方面,更精确的方法使 API 网关能够通过明确定义每个可用 API 资源的 URI 路径来了解 API 的完整 URI 空间。 采用精确的方法,Warehouse API 中 URI 路由的以下配置使用精确匹配 ( = ) 和正则表达式 ( ~ ) 的组合来定义每个有效的 URI。

 

这个配置比较冗长,但更准确地描述了后端服务实现的资源。 这样做的好处是可以保护后端服务免受格式错误的客户端请求的影响,但需要为正则表达式匹配付出一些额外的小开销。 通过此配置,NGINX 会接受一些 URI,并拒绝其他无效 URI:

有效 URI   无效的 URI
/api/仓库/库存   /api/仓库/库存/
/api/仓库/库存/货架/foo   /api/仓库/inventoryfoo
/api/仓库/库存/货架/foo/box/bar   /api/仓库/库存/货架
/api/仓库/库存/货架/-/盒子/-   /api/仓库/库存/货架/foo/bar
/api/仓库/定价/baz   /api/warehouse/定价
    /api/仓库/定价/baz/pub

使用精确的 API 定义使得现有的 API 文档格式能够驱动 API 网关的配置。 可以从OpenAPI 规范(以前称为 Swagger)自动化 NGINX API 定义。 本博文的 Gists 中提供了用于此目的的示例脚本

重写客户端请求以处理重大变更

随着 API 的发展,有时需要进行一些改变,这些改变会破坏严格的向后兼容性并要求客户端进行更新。 一个这样的例子是当 API 资源被重命名或移动时。 与 Web 浏览器不同,API 网关无法向其客户端发送重定向(代码301永久搬迁)命名新地点。 幸运的是,当修改 API 客户端不切实际时,我们可以动态重写客户端请求。

在下面的例子中,我们使用与上面的warehouse_api_simple.conf相同的广泛方法,但在这种情况下,配置正在替换 Warehouse API 的先前版本,其中定价服务作为库存服务的一部分实现。 第 3 行的重写指令将对旧定价资源的请求转换为对新定价服务的请求。

 

响应错误

HTTP API 和基于浏览器的流量之间的一个主要区别是如何将错误传达给客户端。 当 NGINX 部署为 API 网关时,我们将其配置为以最适合 API 客户端的方式返回错误。

顶级 API 网关配置包括一个定义如何处理错误响应的部分。

 

第 23 行的error_page指令指定当请求与任何 API 定义不匹配时,NGINX 将返回400错误请求错误,而不是默认错误404找到错误。 此(可选)行为要求 API 客户端仅向 API 文档中包含的有效 URI 发出请求,并防止未经授权的客户端发现通过 API 网关发布的 API 的 URI 结构。

第24行指的是后端服务本身产生的错误。 未处理的异常可能包含我们不想发送给客户端的堆栈跟踪或其他敏感数据。 此配置通过向客户端发送标准化错误响应来增加进一步的保护级别。

标准化错误响应的完整列表在第 25 行的include指令引用的单独配置文件中定义,其中前几行如下所示。 如果希望使用 JSON 以外的错误格式,可以修改此文件,并更改api_gateway.conf第 26 行的default_type值以进行匹配。 您还可以在每个 API 的策略部分中拥有单独的包含指令,以引用不同的错误响应文件来覆盖全局响应。

有了此配置,客户端对无效 URI 的请求将收到以下响应。[terminal]$ curl -i https://api.example.com/foo HTTP/1.1 400 Bad Request Server: nginx/1.19.5 Content-Type: 应用/json Content-Length: 39 连接:保持活动 {"status":400,"message":"错误请求"}[/terminal]

实现身份验证

在没有某种形式的身份验证来保护的情况下发布 API 的情况并不常见。 NGINX 提供了多种保护 API 和验证 API 客户端的方法。 有关适用于常规 HTTP 请求的方法的信息,请参阅基于 IP 地址的访问控制列表(ACL)、数字证书身份验证HTTP 基本身份验证的文档。 在这里,我们重点关注 API 特定的身份验证方法。

API 密钥认证

API 密钥是客户端和 API 网关所知的共享秘密。 API 密钥本质上是一个长而复杂的密码,作为长期凭证发放给 API 客户端。 创建 API 密钥很简单——只需像本例中那样对随机数进行编码即可。

$ openssl rand -base64 18 7B5zIqmRGXmrJTFmKa99vcit

在顶级 API 网关配置文件api_gateway.conf的第 2 行,我们包含一个名为api_keys.conf的文件,其中包含每个 API 客户端的 API 密钥,由客户端的名称或其他描述标识。 以下是该文件的内容:

API 密钥在映射块内定义。 map指令采用两个参数。 第一个定义了在哪里找到 API 密钥,在本例中是在客户端请求的apikey HTTP 标头中,如$http_apikey变量中捕获的。 第二个参数创建一个新变量( $api_client_name ),并在第一个参数与键匹配的行上将其设置为第二个参数的值。

例如,当客户端出示 API 密钥7B5zIqmRGXmrJTFmKa99vcit时, $api_client_name变量将设置为client_one 。 此变量可用于检查经过身份验证的客户端,并将其包含在日志条目中以进行更详细的审计。 映射块的格式简单,易于集成到从现有凭证存储生成api_keys.conf文件的自动化工作流中。

在这里,我们通过修改“广泛”配置( warehouse_api_simple.conf )来启用 API 密钥身份验证,在策略部分中包含一个auth_request指令,将身份验证决策委托给指定位置。

 

例如,使用auth_request指令(第 7 行),我们可以通过外部身份验证服务器(例如OAuth 2.0 令牌自检)处理身份验证。 在此示例中,我们将验证 API 密钥的逻辑添加到顶级 API 网关配置文件中,形式为以下名为/_validate_apikey位置块。

 

第 30 行的内部指令意味着该位置不能被外部客户端直接访问(只能通过auth_request 访问)。 客户端需要在apikey HTTP 标头中提供他们的 API 密钥。 如果此标头缺失或为空(第 32 行),我们将发送401(未授权的)响应告知客户端需要身份验证。 第 35 行处理 API 密钥与map块中的任何密钥都不匹配的情况 - 在这种情况下, api_keys.conf第 2 行的默认参数将$api_client_name设置为空字符串 - 然后我们发送一个403(禁止)响应告知客户端身份验证失败。 如果上述两个条件都不满足,则 API 密钥有效,并且位置将返回204(无内容回应。

通过此配置,Warehouse API 现在可以实现 API 密钥认证。

$ curl https://api.example.com/api/warehouse/pricing/item001 {"status":401,"message":"未经授权"} $ curl -H "apikey: thisIsInvalid" https://api.example.com/api/warehouse/pricing/item001 {"status":403,"message":"禁止"} $ curl -H "apikey: 7B5zIqmRGXmrJTFmKa99vcit” https://api.example.com/api/warehouse/pricing/item001 {“sku”:“item001”,“price”:179.99}

JWT 认证

JSON Web Tokens(JWT)越来越多地用于 API 身份验证。 本机 JWT 支持是 NGINX Plus 独有的,可实现对 JWT 的验证,如我们博客上使用 JWT 和 NGINX Plus 对 API 客户端进行身份验证中所述。 有关示例实现,请参阅第 2 部分中的控制对特定方法的访问

概括

本系列博客的第一篇详细介绍了部署 NGINX 开源和 NGINX Plus 作为 API 网关的完整解决方案。 您可以从我们的GitHub Gist repo查看和下载本博客中讨论的完整文件集。

查看本系列的其他文章:

  • 第 2 部分探讨了保护后端服务免受恶意或行为不良的客户端攻击的更高级用例。
  • 第 3 部分解释如何将 NGINX 部署为 gRPC 服务的 API 网关。

要试用 NGINX Plus,请立即开始30 天免费试用联系我们讨论您的用例。


“这篇博文可能引用了不再可用和/或不再支持的产品。 有关 F5 NGINX 产品和解决方案的最新信息,请探索我们的NGINX 产品系列。 NGINX 现在是 F5 的一部分。 所有之前的 NGINX.com 链接都将重定向至 F5.com 上的类似 NGINX 内容。”