本文是将 NGINX 开源版和 NGINX Plus 部署为 API 网关系列博文的第二篇。
第 1 部分提供了几个用例的详细配置说明。
本文对这些用例进行了扩展,探讨了一系列可用于保护生产环境中后端 API 服务的安全措施:
本文最初发布于 2018 年,现进行了更新,以反映 API 配置的当前最佳实践——即使用嵌套的 location
块路由请求,而不是重写规则。
注:除非另有说明,否则本文中的所有信息都适用于 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 部分<.htmla>中描述的“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 item001PATCH
/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 接受 GET
和 PATCH
方法,分别支持客户端获取和修改特定物品的价格。(我们还可以选择允许 POST
和 DELETE
方法,以提供定价数据的全生命周期管理。)在本部分,我们对该用例进行扩展,控制特定用户可以发出哪些方法。为 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/item001HTTP/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 模块<.htmla>验证其解析是否正确。
安装 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/pricingHTTP/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 流量管理技术同样被用于支持和保护当今互联网上最繁忙的站点。
查看本系列博文的其他文章:
如欲试用 NGINX Plus 的 API 网关功能,请立即下载 30 天免费试用版,或与我们联系以讨论您的用例。在试用期间,您可使用来自 GitHub Gist 存储库的完整配置文件集。
"This blog post may reference products that are no longer available and/or no longer supported. For the most current information about available F5 NGINX products and solutions, explore our NGINX product family. NGINX is now part of F5. All previous NGINX.com links will redirect to similar NGINX content on F5.com."