乐趣区

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

原文作者:Liam Crilly of F5

原文链接:将 NGINX 部署为 API 网关,第 2 局部:爱护后端服务

转载起源:NGINX 官方网站


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

  • 第 1 局部提供了几个用例的具体配置阐明。
  • 本文对这些用例进行了扩大,探讨了一系列可用于爱护生产环境中后端 API 服务的安全措施:

    • 限流
    • 限定申请办法
    • 利用细粒度的访问控制
    • 管制申请大小
    • 验证申请注释

    本文最后公布于 2018 年,现进行了更新,以反映 API 配置的以后最佳实际——即应用嵌套的 location 块路由申请,而不是重写规定。

  • 第 3 局部解释了如何将 NGINX 开源版和 NGINX Plus 部署为 gRPC 服务的 API 网关。

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

限流

与基于浏览器的客户端不同,单个 API 客户端就可能给您的 API 造成微小的负载,甚至会耗费大量的系统资源,以至其余 API 客户端因而被“排斥”。不仅歹意客户端会形成这种威逼,行为异样或存在缺点的 API 客户端也可能会重复压垮后端。为了防止出现这种状况,咱们用限流来确保每个客户端正当应用 API 并爱护后端服务的资源。

NGINX 能够依据申请的任何属性利用限流。通常应用客户端 IP 地址,但如果为 API 启用身份验证,则通过身份验证的客户端 ID 将是更为牢靠和精确的属性。

限流自身在顶层 API 网关配置文件中定义,并且能够全局、按每个 API 甚至每个 URI 来利用。

include api_backends.conf;
include api_keys.conf;

limit_req_zone $binary_remote_addr zone=client_ip_10rs:1m rate=10r/s; limit_req_zone $http_apikey        zone=apikey_200rs:1m   rate=200r/s;

server {
    access_log /var/log/nginx/api_access.log main; # Each API may also log to a 
                                                  # separate file

    listen 443 ssl;
    server_name api.example.com;

    # TLS config
    ssl_certificate      /etc/ssl/certs/api.example.com.crt;
    ssl_certificate_key  /etc/ssl/private/api.example.com.key;
    ssl_session_cache    shared:SSL:10m;
    ssl_session_timeout  5m;
    ssl_ciphers          HIGH:!aNULL:!MD5;
    ssl_protocols        TLSv1.2 TLSv1.3;

    # API definitions, one per file
    include api_conf.d/*.conf;

    # Error responses
    error_page 404 = @400;         # Invalid paths are treated as bad requests
    proxy_intercept_errors on;     # Do not send backend errors to the client
    include api_json_errors.conf;  # API client friendly JSON error responses
    default_type application/json; # If no content-type then assume JSON
}

在此示例中,第 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`(ServiceUnavailable)` 响应。然而,让 API 客户端明确地晓得本人已超过限流阈值,有助于它们调整本人的行为。为此,咱们应用[`limit_req_status`](https://nginx.org/en/docs/http/ngx_http_limit_req_module.html#limit_req_status) 指令来发送 `429(TooManyRequests)` 响应。

 # Warehouse API
#
location /api/warehouse/ {# Policy configuration here (authentication, rate limiting, logging...)
    #
    access_log /var/log/nginx/warehouse_api.log main;
    limit_req zone=client_ip_10rs;
    limit_req_status 429;

    # URI routing
    #
    location /api/warehouse/inventory {
        limit_except GET {deny all;}
        error_page 403 = @405; # Convert deny response from '403 (Forbidden)'
                               # to '405 (Method Not Allowed)'
        proxy_pass http://warehouse_inventory;
    }

    location /api/warehouse/pricing {
        limit_except GET PATCH {deny all;}
        error_page 403 = @405;
        proxy_pass http://warehouse_pricing;
    }

    return 404; # Catch-all
}

您能够应用 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 办法)。

    # URI routing
    #
    location /api/warehouse/inventory {
        limit_except GET {deny all;}
        error_page 403 = @405; # Convert deny response from '403 (Forbidden)'
                               # to '405 (Method Not Allowed)'
        proxy_pass http://warehouse_inventory;
    }

    location /api/warehouse/pricing {
        limit_except GET PATCH {deny all;}
        error_page 403 = @405; # Convert deny response from '403 (Forbidden)'
                               # to '405 (Method Not Allowed)'
        proxy_pass http://warehouse_pricing;
    }

应用此配置后,未应用第 22 行所列办法向定价 service 收回的申请(以及未应用第 13 行所列办法对库存 service 进行申请)将被回绝,并且不会传递到后端 service。NGINX 发送 405`(MethodNotAllowed) 响应,以告诉 API 客户端确切的谬误类型,如以下控制台跟踪所示。在须要遵循“最小披露”的安全策略时,可应用 [error_page](https://nginx.org/en/docs/http/ngx_http_core_module.html#error_page) 指令将此响应转换为信息量较少的谬误,例如400(BadRequest)`。

$ 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 的属性来执行细粒度的访问控制。

咱们在此处提供了两个相干示例:

  • 第一个示例为管制对特定 API 资源的拜访,扩大了第 1 局部中介绍的配置,并应用 API Key 认证形式验证给定 API 客户端是否在容许名单(allowlist)中。
  • 第二个示例为限定客户端应用哪些 HTTP 办法。它实现了第 1 局部中提到的 JWT 认证形式,应用自定义申明来辨认符合条件的 API 客户端。(请留神,JWT 反对是 NGINX Plus 的独有性能。)

当然,其余认证形式也实用于这些示例中的用例,例如 HTTP Basic 认证和 OAuth 2.0 令牌自省。

管制对特定资源的拜访

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

map $api_client_name $is_infrastructure {
    default       0;

    "client_one"  1;
    "client_six"  1;
}

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

# Warehouse API
#
location /api/warehouse/ {# Policy configuration here (authentication, rate limiting, logging...)
    #
    access_log /var/log/nginx/warehouse_api.log main;
    auth_request /_validate_apikey;
 
    # URI routing
    #
    location /api/warehouse/inventory {proxy_pass http://warehouse_inventory;}
 
    location = /api/warehouse/inventory/audit {if ($is_infrastructure = 0) {return 403; # Forbidden (not infrastructure)
        }
        proxy_pass http://warehouse_inventory;
    }
 
    location /api/warehouse/pricing {proxy_pass http://warehouse_pricing;}
 
    return 404; # Catch-all
}

请留神,第 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。当初,咱们扩大了访问控制逻辑,以便只有管理员能力进行更改。

# Access to write operations is evaluated by JWT claim 'admin'
map $request_method $admin_permitted_method {
    "GET"     true;
    "HEAD"    true;
    "OPTIONS" true;
    default   $jwt_claim_admin;
}

map 块(被增加到 api_gateway.conf 的底部)将申请办法 ($request_method) 作为输出并生成一个新变量 $admin_permitted_method。只读办法始终容许(第 62-64 行),但对写入操作的拜访取决于 JWT 中admin 申明的值(第 65 行)。咱们当初扩大了 Warehouse API 配置,以确保只有管理员能力更改定价。

# Warehouse API
#
location /api/warehouse/ {# Policy configuration here (authentication, rate limiting, logging...)
    #
    access_log /var/log/nginx/warehouse_api.log main;
    auth_jwt "Warehouse API";
    auth_jwt_key_file /etc/nginx/idp_jwks.json;
 
    # URI routing
    #
    location /api/warehouse/inventory {
        limit_except GET {deny all;}
        error_page 403 = @405; # Convert deny response from '403 (Forbidden)'
                               # to '405 (Method Not Allowed)'
        proxy_pass http://warehouse_inventory;
    }
    
    location /api/warehouse/pricing {
        limit_except GET PATCH {deny all;}
        if ($admin_permitted_method != "true") {return 403;}
        error_page 403 = @405; # Convert deny response from '403 (Forbidden)'
                               # to '405 (Method Not Allowed)'
        proxy_pass http://warehouse_pricing;
    }
 
    return 404; # Catch-all
}

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,咱们会设置一个较低的值。

# Warehouse API
#
location /api/warehouse/ {# Policy configuration here (authentication, rate limiting, logging...)
    #
    access_log /var/log/nginx/warehouse_api.log main;
    client_max_body_size 16k;

第 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 [email protected]/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_import json_validation.js;
js_set $json_validated json_validation.parseRequestBody;

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

export default {parseRequestBody};

function parseRequestBody(r) {
    try {if (r.variables.request_body) {JSON.parse(r.variables.request_body);
        }
        return r.variables.upstream;
    } catch (e) {r.error('JSON.parse exception');
        return '127.0.0.1:10415'; // Address for error response
    }
}

parseRequestBody函数尝试应用 JSON.parse 办法(第 6 行)解析申请注释。如果解析胜利,则返回此申请所需上游 group 的名称(第 8 行)。如果无奈解析申请注释(导致异样),则返回本地服务器地址(第 11 行)。return指令将填充 $json_validated 变量,以便咱们能够应用它来确定将申请发送到何处。

# URI routing
    #
    location /api/warehouse/inventory {proxy_pass http://warehouse_inventory;}

    location /api/warehouse/pricing {
        set $upstream warehouse_pricing;
        mirror /_get_request_body;        # Force early read
        client_body_in_single_buffer on;  # Minimize memory copy operations 
                                          # on request body
        client_body_buffer_size      16k; # Largest body to keep in memory 
                                          # (before writing to file)
        client_max_body_size         16k;
        proxy_pass http://$json_validated$request_uri;
    }

在 Warehouse API 的 URI 路由局部,咱们在第 22 行批改了 proxy_pass 指令。它将申请传递给后端 API service,如后面局部中探讨的 Warehouse API 配置一样,然而当初应用 $json_validated 变量作为指标地址。如果客户端注释被胜利解析为 JSON,那么咱们将代理到第 15 行定义的上游 group。然而,如果出现异常,咱们将应用返回值 127.0.0.1:10415 向客户端发送谬误响应。

server {
    listen 127.0.0.1:10415;
    return 415; # Unsupported media type
    include api_json_errors.conf;
}

当申请被代理到这个虚构服务器时,NGINX 将向客户端发送415`(UnsupportedMediaType)`) 响应。

有了这个残缺的配置,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 变量。

location /api/warehouse/pricing {
        set $upstream warehouse_pricing;
        mirror /_get_request_body;        # Force early read
        client_body_in_single_buffer on;  # Minimize memory copy operations 
                                          # on request body
        client_body_buffer_size      16k; # Largest body to keep in memory 
                                          # (before writing to file)
        client_max_body_size         16k;
        proxy_pass http://$json_validated$request_uri;
    }

第 17 行和第 19 行的指令管制 NGINX 如何在外部解决申请注释。咱们将 client_body_buffer_size 设置为与 client_max_body_size 雷同的大小,这样申请注释就不会写入磁盘。这样做有助于最大限度地缩小磁盘 I/O 操作,从而进步整体性能,但代价是内存利用率会有所增加。对于大多数申请注释较小的 API 网关用例,这是一个不错的折衷方案。

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

# Dummy location used to populate $request_body for JSON validation
    location = /_get_request_body {return 204;}

此地位只发送 204`(No`Content) 响应。此响应与镜像申请相干,因而被疏忽,对原始客户端申请的解决所减少的开销也能够忽略不计。

总结

本文是将 NGINX 开源版和 NGINX Plus 部署为 API 网关系列博文的第二篇,次要关注如何爱护生产环境中的后端 API service 免受歹意和行为异样的客户端的影响。NGINX 所应用的 API 流量治理技术同样被用于反对和爱护当今互联网上最忙碌的站点。

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

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

更多资源

想要更及时全面地获取 NGINX 相干的技术干货、互动问答、系列课程、流动资源?

请返回 NGINX 开源社区:

  • 官网:https://www.nginx.org.cn/
  • 微信公众号:https://mp.weixin.qq.com/s/XVE5yvDbmJtpV2alsIFwJg
  • 微信群:https://www.nginx.org.cn/static/pc/images/homePage/QR-code.png?v=1621313354
  • B 站:https://space.bilibili.com/628384319
退出移动版