NGINX.COM
Web Server Load Balancing with NGINX Plus

本文是将 NGINX 开源版和 NGINX Plus 部署为 API 网关系列博文的第二篇。

:除非另有说明,否则本文中的所有信息都适用于 NGINX 开源版和 NGINX Plus。为了便于阅读,下文将 NGINX 开源版和 NGINX Plus 统称为“NGINX”。

 

限流

与基于浏览器的客户端不同,单个 API 客户端就能够给您的 API 造成巨大的负载,甚至会消耗大量的系统资源,以致其他 API 客户端因此被“排挤”。不仅恶意客户端会构成这种威胁,行为异常或存在缺陷的 API 客户端也可能会反复压垮后端。为了防止出现这种情况,我们用限流来确保每个客户端合理使用 API 并保护后端服务的资源。

NGINX 可以根据请求的任何属性应用限流。通常使用客户端 IP 地址,但如果为 API 启用身份验证,则经过身份验证的客户端 ID 将是更为可靠和准确的属性。

限流本身在顶层 API 网关配置文件中定义,并且可以全局、按每个 API 甚至每个 URI 来应用。

在此示例中,第 4 行的 limit_req_zone 指令为每个客户端 IP 地址 ($binary_remote_addr) 定义每秒 10 个请求的限流,第 5 行的 limit_req_zone 指令为每个经过身份验证的客户端 ID ($http_apikey) 定义每秒 200 个请求的限流。该示例说明了我们可以定义多个限流,而不受它们所应用位置的约束。一个 API 可以同时应用多个限流,或者对不同的资源应用不同的限流。

在下面的配置段中,我们使用 limit_req 指令来应用本系列博文第 1 部分中描述的“Warehouse API”策略部分中的第一个限流。默认情况下,当超过限流阈值时,NGINX 会发送 503 (Service Unavailable) 响应。然而,让 API 客户端明确地知道自己已超过限流阈值,有助于它们调整自己的行为。为此,我们使用 limit_req_status 指令来发送 429 (Too Many Requests) 响应。

您可以使用 limit_req 指令的附加参数来微调 NGINX 执行限流的方式。例如,当超过限流阈值时,可以让请求排队而不是直接拒绝它们,从而使请求速率有时间降至定义的限制之下。有关微调限流阈值的更多信息,请参阅我们的博文《使用NGINX 和 NGINX Plus 实现限流》

 

限定请求方法

对于 RESTful API,HTTP 方法是每个 API 调用的重要组成部分,对 API 定义非常重要。以 Warehouse API 的定价服务 service 为例:

  • GET /api/warehouse/pricing/item001        returns the price of item001
  • PATCH /api/warehouse/pricing/item001   changes the price of item001

我们可以更新 Warehouse API 中的 URI 路由定义,以便在对定价 service 的请求中只接受这两个 HTTP 方法(并且在对库存 service 的请求中只接受 GET 方法)。

使用此配置后,未使用第 22 行所列方法向定价 service 发出的请求(以及未使用第 13 行所列方法对库存 service 进行请求)将被拒绝,并且不会传递到后端 service 。NGINX 发送 405 (Method Not Allowed) 响应,以通知 API 客户端确切的错误类型,如以下控制台跟踪所示。在需要遵循“最小披露”的安全策略时,可使用 error_page 指令将此响应转换为信息量较少的错误,例如 400 (Bad Request)

$ curl https://api.example.com/api/warehouse/pricing/item001
{"sku":"item001","price":179.99}
$ curl -X DELETE https://api.example.com/api/warehouse/pricing/item001
{"status":405,"message":"Method not allowed"}

 

应用细粒度的访问控制

本系列博文的第 1 部分介绍了如何通过启用身份验证选项(例如 API 密钥JSON Web Tokens (JWT))保护 API 免受未经授权的访问。我们可以使用经过身份验证的 ID 或经过身份验证的 ID 的属性来执行细粒度的访问控制。

我们在此处提供了两个相关示例:

当然,其他认证方式也适用于这些示例中的用例,例如 HTTP Basic 认证OAuth 2.0 令牌自省

控制对特定资源的访问

假设我们只允许“基础设施客户端”访问 Warehouse API 库存 service 的 audit 资源。启用 API Key 认证方式后,我们使用 map 块创建基础设施客户端名称的允许名单,以便在使用相应的 API Key 时变量 $is_infrastructure 的计算结果为 1

在 Warehouse API 的定义中,我们为库存 audit 资源添加了一个 location 块(第 15-20 行)。if 块可确保只有基础设施客户端可以访问该资源。

请注意,第 15 行的 location 指令使用 =(等号)修饰符与 audit 资源进行精确匹配。精确匹配优先于用于其他资源的默认路径前缀定义。以下跟踪显示了在使用此配置的情况下,不在允许名单上的客户端如何无法访问库存 audit 资源。所示 API Key 属于 client_two(如第 1 部分中所定义)。

$ curl -H "apikey: QzVV6y1EmQFbbxOfRCwyJs35" https://api.example.com/api/warehouse/inventory/audit
{"status":403,"message":"Forbidden"}

控制对特定方法的访问

如上所述,定价 service 接受 GETPATCH 方法,分别支持客户端获取和修改特定物品的价格。(我们还可以选择允许 POSTDELETE 方法,以提供定价数据的全生命周期管理。)在本部分,我们对该用例进行扩展,控制特定用户可以发出哪些方法。为 Warehouse API 启用 JWT 身份验证后,每个客户端的权限都被编码为自定义声明。发给授权更改定价数据的管理员的 JWT 包含声明 "admin":true。现在,我们扩展了访问控制逻辑,以便只有管理员才能进行更改。

map 块(被添加到 api_gateway.conf 的底部)将请求方法 ($request_method) 作为输入并生成一个新变量 $admin_permitted_method。只读方法始终允许(第 62-64 行),但对写入操作的访问取决于 JWT 中 admin 声明的值(第 65 行)。我们现在扩展了 Warehouse API 配置,以确保只有管理员才能更改定价。

Warehouse API 要求所有客户端都提供有效的 JWT(第 7 行)。我们还通过评估 $admin_permitted_method 变量(第 25 行)来检查是否允许写入操作。再次提醒,JWT 身份验证是 NGINX Plus 的独有功能。

 

控制请求大小

HTTP API 通常使用请求正文来包含后端 API service 要处理的指令和数据。XML/SOAP API 以及 JSON/REST API 也是如此。因此,请求正文可能会构成后端 API service 的攻击向量,当后端 API service 处理超大的请求正文时,可能容易受到缓冲区溢出 攻击。

默认情况下,NGINX 拒绝正文大于 1MB 的请求。对于专门处理大型负载(例如图像处理)的 API,此值可以增加,但对于大多数 API,我们会设置一个较低的值。

第 7 行的 client_max_body_size 指令限制了请求正文的大小。有了此配置,我们就可以比较 API 网关在接收到两个不同的 PATCH 定价 service 请求时的行为。第一个 curl 命令发送一小段 JSON 数据,而第二个命令则尝试发送一个大文件 (/etc/services) 的内容。

$ curl -iX PATCH -d '{"price":199.99}' https://api.example.com/api/warehouse/pricing/item001
HTTP/1.1 204 No Content
Server: nginx/1.19.5
Connection: keep-alive

$ curl -iX PATCH -d@/etc/services https://api.example.com/api/warehouse/pricing/item001
HTTP/1.1 413 Request Entity Too Large
Server: nginx/1.19.5
Content-Type: application/json
Content-Length: 45
Connection: close

{"status":413,"message":"Payload too large"}

 

验证请求正文

[编者按 —— 以下用例是 NGINX JavaScript 模块几个用例之一。查看完整列表,请参阅《NGINX JavaScript 模块的用例》]。

除了容易受到大型请求正文的缓冲区溢出攻击之外,后端 API service 还容易受到包含无效或意外数据的正文的影响。对于需要请求正文具有正确格式的 JSON 的应用,我们可以在将 JSON 数据代理到后端 API service 之前,使用 NGINX JavaScript 模块验证其解析是否正确。

安装 JavaScript 模块后,我们使用 js_import 指令来引用包含 JSON 数据验证函数的 JavaScript 代码的文件。

js_set 指令定义了一个新变量 $json_validated,通过调用 parseRequestBody 函数对其进行计算。

parseRequestBody 函数尝试使用 JSON.parse 方法(第 6 行)解析请求正文。如果解析成功,则返回此请求所需上游 group 的名称(第 8 行)。如果无法解析请求正文(导致异常),则返回本地服务器地址(第 11 行)。return 指令将填充 $json_validated 变量,以便我们可以使用它来确定将请求发送到何处。

在 Warehouse API 的 URI 路由部分,我们在第 22 行修改了 proxy_pass 指令。它将请求传递给后端 API service ,如前面部分中讨论的 Warehouse API 配置一样,但是现在使用 $json_validated 变量作为目标地址。如果客户端正文被成功解析为 JSON,那么我们将代理到第 15 行定义的上游 group。但是,如果出现异常,我们将使用返回值 127.0.0.1:10415 向客户端发送错误响应。

当请求被代理到这个虚拟服务器时,NGINX 将向客户端发送 415 (Unsupported Media Type)) 响应。

有了这个完整的配置,NGINX 将只在请求具有正确格式的 JSON 正文时才将其代理到后端 API service 。

$ curl -iX POST -d '{"sku":"item002","price":85.00}' https://api.example.com/api/warehouse/pricing
HTTP/1.1 201 Created
Server: nginx/1.19.5
Location: /api/warehouse/pricing/item002

$ curl -X POST -d 'item002=85.00' https://api.example.com/api/warehouse/pricing
{"status":415,"message":"Unsupported media type"}

关于 $request_body 变量的说明

JavaScript 函数 parseRequestBody 使用 $request_body 变量来执行 JSON 解析。但是,NGINX 默认不赋值此变量,只是将请求正文流式传输到后端而创建中间副本。我们通过在 URI 路由部分(第 16 行)使用 mirror 指令,创建客户端请求的副本,并赋值 $request_body 变量。

第 17 行和第 19 行的指令控制 NGINX 如何在内部处理请求正文。我们将 client_body_buffer_size 设置为与 client_max_body_size 相同的大小,这样请求正文就不会写入磁盘。这样做有助于最大限度地减少磁盘 I/O 操作,从而提高整体性能,但代价是内存利用率会有所增加。对于大多数请求正文较小的 API 网关用例,这是一个不错的折衷方案。

如前所述,mirror 指令会创建客户端请求的副本。除了赋值 $request_body 之外,我们不需要此副本,因此我们将其发送到我们在顶层 API 网关配置的 server 块中定义的“死胡同(dead end)”位置 (/_get_request_body)。

此位置只发送 204 (No Content)响应。此响应与镜像请求相关,因此被忽略,对原始客户端请求的处理所增加的开销也可以忽略不计。

 

总结

本文是将 NGINX 开源版和 NGINX Plus 部署为 API 网关系列博文的第二篇,主要关注如何保护生产环境中的后端 API service 免受恶意和行为异常的客户端的影响。NGINX 所使用的 API 流量管理技术同样被用于支持和保护当今互联网上最繁忙的站点

查看本系列博文的其他文章:

  • 第 1 部分解释了如何在一些基本的 API 网关用例中配置 NGINX。
  • 第 3 部分解释了如何将 NGINX 部署为 gRPC 服务的 API 网关。

如欲试用 NGINX Plus 的 API 网关功能,请立即下载 30 天免费试用版,或与我们联系以讨论您的用例。在试用期间,您可使用来自 GitHub Gist 存储库的完整配置文件集。

Hero image
将 NGINX 部署为 API 网关

这本免费电子书更新于 2022 年,您将通过这本书了解到如何将 NGINX 部署为 API 网关

关于作者

Liam Crilly

F5 产品管理总监

关于 F5 NGINX

F5, Inc. 是备受欢迎的开源软件 NGINX 背后的商业公司。我们为现代应用的开发和交付提供一整套技术。我们的联合解决方案弥合了 NetOps 和 DevOps 之间的横沟,提供从代码到用户的多云应用服务。访问 nginx-cn.net 了解更多相关信息。