博客 | NGINX

将 NGINX 部署为 API 网关,第 2 部分: 保护后端服务

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

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

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

速率限制

与基于浏览器的客户端不同,单个 API 客户端能够对您的 API 施加巨大的负载,甚至会消耗大量系统资源,导致其他 API 客户端被有效锁定。 不仅恶意客户端会构成这种威胁:行为不当或有缺陷的 API 客户端可能会进入使后端不堪重负的循环。 为了防止这种情况,我们采用速率限制来确保每个客户端的公平使用并保护后端服务的资源。

NGINX 可以根据请求的任何属性应用速率限制。 通常使用客户端 IP 地址,但是当为 API 启用身份验证时,经过身份验证的客户端 ID 是更可靠、更准确的属性。

速率限制本身在顶级 API 网关配置文件中定义,然后可以全局应用、基于每个 API 甚至每个 URI 应用。

 

在此示例中,第 4 行的limit_req_zone指令为每个客户端 IP 地址 ( $binary_remote_addr ) 定义了每秒 10 个请求的速率限制,第 5 行的指令为每个经过身份验证的客户端 ID ( $http_apikey ) 定义了每秒 200 个请求的限制。 这说明了我们如何可以独立于其应用位置来定义多个速率限制。 一个API可以同时应用多个速率限制,或者针对不同的资源应用不同的速率限制。

然后在以下配置片段中我们使用limit_req指令来应用第 1 部分<.htmla>中描述的“Warehouse API”的策略部分中的第一个速率限制。 默认情况下,NGINX 发送 503 (服务 不可用) 当超出速率限制时做出响应。 但是,如果 API 客户端明确地知道他们已经超出了速率限制,那么他们就可以修改其行为,这将很有帮助。 为此,我们使用limit_req_status指令来发送429请求过多响应。

 

您可以使用limit_req指令的附加参数来微调 NGINX 强制执行速率限制的方式。 例如,可以对请求进行排队,而不是在超出限制时直接拒绝它们,从而留出时间让请求率降至定义的限制以下。 有关微调速率限制的更多信息,请参阅我们博客上的使用 NGINX 和 NGINX Plus 进行速率限制

执行特定请求方法

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

  • GET /api/warehouse/pricing/item001返回 item001 的价格
  • PATCH /api/warehouse/pricing/item001更改 item001 的价格

我们可以更新 Warehouse API 中的 URI 路由定义,以仅接受对定价服务的请求中的这两种 HTTP 方法(以及对库存服务的请求中的GET方法)。

 

有了这种配置,使用第 22 行未列出的方法对定价服务发出的请求(以及使用第 13 行未列出的方法对库存服务发出的请求)将被拒绝,并且不会传递给后端服务。 NGINX 发送405(方法不允许响应来告知 API 客户端错误的确切性质,如下面的控制台跟踪所示。 当需要最低限度披露安全策略时,可以使用error_page指令将此响应转换为信息量较少的错误,例如400(错误的请求)

$ 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":"方法不允许"}

应用细粒度访问控制

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

这里我们展示两个这样的例子。

当然,其他身份验证方法也适用于这些示例用例,例如HTTP 基本身份验证OAuth 2.0 令牌自检

控制对特定资源的访问

假设我们只允许“基础设施客户端”访问 Warehouse API 库存服务的审计资源。 启用 API 密钥身份验证后,我们使用map块创建基础设施客户端名称的允许列表,以便变量$is_infrastructure计算结果为1当使用相应的 API 密钥时。

 

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

 

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

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

控制对特定方法的访问

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

 

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

 

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

控制请求大小

HTTP API 通常使用请求正文来包含后端 API 服务要处理的指令和数据。 XML/SOAP API 和 JSON/REST API 都是如此。 因此,请求主体可能对后端 API 服务构成攻击媒介,在处理非常大的请求主体时,后端 API 服务可能容易受到缓冲区溢出攻击。

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

 

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

$ curl -iX PATCH -d '{"price":199.99}' https://api.example.com/api/warehouse/pricing/item001 HTTP/1.1 204 无内容 服务器:nginx/1.19.5 连接:keep-alive $ curl -iX PATCH -d@/etc/services https://api.example.com/api/warehouse/pricing/item001 HTTP/1.1 413 请求实体太大 服务器:nginx/1.19.5 内容类型: 应用/json 内容长度: 45 连接:关闭 {"status":413,"message":"Payload 太大"}

验证请求主体

[编辑器– 以下用例是 NGINX JavaScript 模块的几个用例之一。 有关完整列表,请参阅NGINX JavaScript 模块的用例 ]

除了容易受到大型请求主体的缓冲区溢出攻击之外,后端 API 服务还容易受到包含无效或意外数据的主体的攻击。 对于需要在请求正文中使用正确格式的 JSON 的应用,我们可以使用NGINX JavaScript 模块<.htmla> 来验证 JSON 数据在代理到后端 API 服务之前是否被正确解析。

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

 

js_set指令定义了一个新变量$json_validated ,该变量通过调用parseRequestBody函数进行评估。

 

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

 

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

 

当请求被代理到这个虚拟服务器时,NGINX 会发送415(不支持的媒体类型)响应给客户端。

完成上述配置后,只有 JSON 主体格式正确,NGINX 才会代理请求后端 API 服务。

$ curl -iX POST -d '{"sku":"item002","price":85.00}' https://api.example.com/api/warehouse/pricing HTTP/1.1 201 已创建 服务器:nginx/1.19.5 位置:/api/warehouse/pricing/item002 $ curl -X POST -d 'item002=85.00' https://api.example.com/api/warehouse/pricing {"status":415,"message":"不支持的媒体类型"}

关于$request_body变量的说明

JavaScript 函数parseRequestBody使用$request_body变量执行 JSON 解析。 但是,NGINX 默认不会填充此变量,而只是将请求主体流式传输到后端,而不进行中间复制。 通过使用 URI 路由部分 (第 16 行) 内的镜像指令,我们创建了客户端请求的副本,并因此填充$request_body变量。

 

第 17 行和 19 行的指令控制 NGINX 如何在内部处理请求正文。 我们将client_body_buffer_size设置为与client_max_body_size相同的大小,以便请求主体不会写入磁盘。 这通过最小化磁盘 I/O 操作来提高整体性能,但却以额外的内存利用率为代价。 对于大多数请求体较小的 API 网关用例来说,这是一个很好的折衷方案。

如上所述,镜像指令创建客户端请求的副本。 除了填充$request_body之外,我们不需要这个副本,因此我们将其发送到我们在顶级 API 网关配置中的服务器块中定义的“死胡同”位置( /_get_request_body )。

 

此位置的作用只是发送204(无内容回应。 由于此响应与镜像请求相关,因此它会被忽略,从而给原始客户端请求的处理增加的开销可以忽略不计。

概括

在本系列关于部署 NGINX 开源和 NGINX Plus 作为 API 网关的第二篇博文中,我们重点关注了在生产环境中保护后端 API 服务免受恶意和行为不当的客户端攻击的挑战。 NGINX 使用与当今互联网上最繁忙的网站支持和保护相同的技术来管理 API 流量。

查看本系列的其他文章:

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

要尝试 NGINX Plus 作为 API 网关,请立即开始30 天免费试用联系我们讨论您的用例。 在试用期间,请使用我们GitHub Gist 存储库中的完整配置文件。


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