现代应用架构的核心是 HTTP API。HTTP 支持快速构建和轻松维护应用。HTTP API 提供了一个通用接口,因此不必考虑应用的规模大小,无论是单独用途的微服务还是大型综合应用。 HTTP 不仅可以支持超大规模互联网,也可用于提供可靠和高性能的 API 交付。
作为领先的高性能、轻量级反向代理和负载均衡器,NGINX 拥有处理 API 流量所需的高级 HTTP 处理功能。这使得 NGINX 成为构建 API 网关的理想平台。本文描述了一些常见的 API 网关用例,并展示了如何以高效、可扩展和易于维护的方式配置 NGINX。我们描述了一套完整的配置,该配置可构成生产环境部署的基础。
Warehouse API 简介
API 网关的主要功能是为多个 API(无论它们在后端如何实施或部署)提供统一的一致的入口点。并非所有 API 都是微服务应用。我们的 API 网关需要管理现有的 API、单体应用和正在过渡到微服务的应用。
本文假定了一个用于库存管理的 API,名为 “Warehouse API”。我们使用示例配置代码来说明不同的用例。Warehouse API 是一个 RESTful API,它接收 JSON 请求并且生成 JSON 响应。不过,在部署为 API 网关时,NGINX 并不限制只能使用 JSON,因为 NGINX 的部署与架构风格和 API 本身使用的数据格式是无关的。
Warehouse API 是由几个独立的微服务集合在一起后,作为单个 API 发布而实现的。库存和价格资源分别由不同的服务实现并部署到不同的后端。因此,API 的路径结构是:
api
└── warehouse
├── inventory
└── pricing
举例来说,如要查询当前的仓库库存,客户端应用将向 /api/warehouse/inventory 发送 HTTP GET
请求。
面向多个应用的 API 网关架构
组织 NGINX 配置
将 NGINX 用作 API 网关的一个优势是,它不仅可以很好地担任 API 网关这一角色,同时还可以充当现有 HTTP 流量的反向代理、负载均衡器和 Web 服务器。如果 NGINX 已经是应用交付架构的一部分,那么通常不需要再部署一个独立的 API 网关。然而,API 网关的一些默认行为与基于浏览器流量的行为有所不同。因此,我们将 API 网关配置与基于浏览器流量的任何现有(或未来)配置分离。
为了实现这种分离,我们创建了一个支持多用途 NGINX 实例的配置,并提供了一个易于使用的结构,用于通过 CI/CD 流水线实现自动化配置部署。/etc/nginx 下的生成目录结构如下所示。
etc/
└── nginx/
├── api_conf.d/ ………………………………… Subdirectory for per-API configuration
│ └── warehouse_api.conf …… Definition and policy of the Warehouse API
├── api_backends.conf ………………… The backend services (upstreams)
├── api_gateway.conf …………………… Top-level configuration for the API gateway server
├── api_json_errors.conf ………… HTTP error responses in JSON format
├── 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
指令来提高可读性及实现部分配置的自动化。
28 include /etc/nginx/api_gateway.conf; # All API gateway configuration
29 include /etc/nginx/conf.d/*.conf; # Regular web traffic
api_gateway.conf 文件定义了将 NGINX 作为 API 网关暴露给客户端的 virtual server。此配置在单个入口点 https://api.example.com/(第 9 行)暴露 API 网关发布的所有 API,这些 API 受第 12 行到第 17 行配置的 TLS 保护。请注意,此配置是纯 HTTPS —— 没有明文 HTTP 监听器。我们假定 API 客户端知道正确的入口点并默认建立的是 HTTPS 连接。
此配置是静态的 —— 各个 API 及其后端服务的详细配置在第 20 行 include
指令引用的文件中指定。第 23 行至第 26 行涉及错误处理,将在下面的 “响应错误” 部分进行讨论。
1 include api_backends.conf;
2 include api_keys.conf;
3
4 server {
5 access_log /var/log/nginx/api_access.log main; # Each API may also log to a
6 # separate file
7
8 listen 443 ssl;
9 server_name api.example.com;
10
11 # TLS config
12 ssl_certificate /etc/ssl/certs/api.example.com.crt;
13 ssl_certificate_key /etc/ssl/private/api.example.com.key;
14 ssl_session_cache shared:SSL:10m;
15 ssl_session_timeout 5m;
16 ssl_ciphers HIGH:!aNULL:!MD5;
17 ssl_protocols TLSv1.2 TLSv1.3;
18
19 # API definitions, one per file
20 include api_conf.d/*.conf;
21
22 # Error responses
23 error_page 404 = @400; # Treat invalid paths as bad requests
24 proxy_intercept_errors on; # Do not send backend errors to client
25 include api_json_errors.conf; # API client friendly JSON error
26 default_type application/json; # If no content-type, assume JSON
27 }
单体服务与微服务 API 的后端
一些 API 可能由单个后端实现,尽管出于弹性或负载均衡方面的考虑,我们通常希望有多个。我们通过微服务 API 为每个 service 定义单独的后端;它们共同形成完整的 API 功能。此处,Warehouse API 被部署为两个独立的 service,每个 service 都有多个后端。
upstream warehouse_inventory {
zone inventory_service 64k;
server 10.0.0.1:80;
server 10.0.0.2:80;
server 10.0.0.3:80;
}
upstream warehouse_pricing {
zone pricing_service 64k;
server 10.0.0.7:80;
server 10.0.0.8:80;
server 10.0.0.9:80;
}
由 API 网关发布的所有后端 API 服务都在 api_backends.conf 中定义。此处,我们在每个 upstream 块中使用多个 “IP 地址 – 端口” 组合(也可以使用主机名)来指示 API 代码的部署位置。NGINX Plus 用户还可以利用动态 DNS 负载均衡功能将新的后端自动添加到运行时配置中。
定义 Warehouse API
Warehouse API 由嵌套配置中的一些 location 块定义,如下例所示。外部 location
块 (/api/warehouse) 标识基本路径,嵌套位置在该路径下的 URI,指定路由到后端 API service。我们可以使用外部块定义适用于整个 API 的通用策略(在此示例中,为第 6 行的日志记录配置)。
# Warehouse API
#
location /api/warehouse/ {
# Policy configuration here (authentication, rate limiting, logging...)
#
access_log /var/log/nginx/warehouse_api.log main;
# URI routing
#
location /api/warehouse/inventory {
proxy_pass http://warehouse_inventory;
}
location /api/warehouse/pricing {
proxy_pass http://warehouse_pricing;
}
return 404; # Catch-all
}
NGINX 拥有一个高效而又灵活的系统,用于将请求 URI 与配置的一部分相匹配。location
指令的顺序并不重要,系统会选择匹配度最高的指令。此处,第 10 行和第 14 行的嵌套位置定义了两个比外部 location
块更具体的 URI;每个嵌套块中的 proxy_pass 指令将请求路由到适当的 upstream group。除非需要为某些 URI 提供更具体的策略,否则策略配置从外部 location 继承。
任何与其中的一个嵌套位置不匹配的 URI 都由外部 location 处理,其中包括一个 catch-all 指令(第 18 行),该指令为所有无效 URI 返回响应 404
(Not
Found)
。
为 API 选择宽泛定义或精确定义
API 有两种定义方法 —— 宽泛和精确。每个 API 最合适哪种定义方法取决于 API 的安全要求,以及后端 service 是否需要处理无效 URI。
在上面的 warehouse_api_simple.conf 中,我们对 Warehouse API 使用了宽泛定义方法,在第 10 行和第 14 行定义了 URI 前缀,这样以其中一个前缀开头的 URI 就会被代理到适当的后端 service。通过这种宽泛的、基于前缀的 location 匹配,对以下 URI 的 API 请求都是有效的:
/api/warehouse/inventory
/api/warehouse/inventory/
/api/warehouse/inventory/foo
/api/warehouse/inventoryfoo
/api/warehouse/inventoryfoo/bar/
如果只需考虑将每个请求代理到正确的后端 service,则宽泛的定义方法可提供最快的处理速度和最紧凑的配置。另一方面,更精确的方法可以显式定义每个可用 API 资源的 URI 路径,从而使 API 网关能够了解 API 完整的 URI 空间。通过采用精确定义方法,Warehouse API 中的以下 URI 路由配置可使用精确匹配 (=
) 和正则表达式 (~
) 组合来定义每个有效的 URI。
# URI routing
#
location = /api/warehouse/inventory { # Complete inventory
proxy_pass http://warehouse_inventory;
}
location ~ ^/api/warehouse/inventory/shelf/[^/]+$ { # Shelf inventory
proxy_pass http://warehouse_inventory;
}
location ~ ^/api/warehouse/inventory/shelf/[^/]+/box/[^/]*$ { # Box on shelf
proxy_pass http://warehouse_inventory;
}
location ~ ^/api/warehouse/pricing/[^/]+$ { # Price for specific item
proxy_pass http://warehouse_pricing;
}
这种配置较为冗长,但更准确地描述了后端 service 实现的资源。这样做的好处是可以保护后端 service 免受格式不正确的客户端请求的影响,而代价是产生少许额外的正则表达式匹配开销。有了此配置,NGINX 将接受一些 URI 并拒绝其他无效的 URI:
有效的 URI | 无效的 URI |
---|---|
/api/warehouse/inventory | /api/warehouse/inventory/ |
/api/warehouse/inventory/shelf/foo | /api/warehouse/inventoryfoo |
/api/warehouse/inventory/shelf/foo/box/bar | /api/warehouse/inventory/shelf |
/api/warehouse/inventory/shelf/-/box/- | /api/warehouse/inventory/shelf/foo/bar |
/api/warehouse/pricing/baz | /api/warehouse/pricing |
/api/warehouse/pricing/baz/pub | |
通过使用精确的 API 定义,现有的 API 归档格式可驱动 API 网关的配置。可以实现通过 OpenAPI 规范(以前称为 Swagger)自动定义 NGINX API。本文的 Gists 中提供了一个用于此目的的示例脚本。
重写客户端请求以处理重大变更
随着 API 的发展,有时需要进行变更,会打破严格的向后兼容性并要求更新客户端。例如重命名或移动某个 API 资源的时候,与 Web 浏览器不同,API 网关无法向客户端发送重定向(代码 301
(Moved
Permanently)
)来命名新位置。幸运的是,如果无法修改 API 客户端,我们可以动态地重写客户端请求。
在下面的示例中,我们使用与上文 warehouse_api_simple.conf 相同的宽泛定义方法,但在本例中,配置替换了以前版本的 Warehouse API,其中定价 service 作为库存 service 的一部分实现。第 3 行的 rewrite 指令将对旧定价资源的请求转换为对新定价 service 的请求。
# Rewrite rules
#
rewrite ^/api/warehouse/inventory/item/price/(.*) /api/warehouse/pricing/$1;
# Warehouse API
#
location /api/warehouse/ {
# Policy configuration here (authentication, rate limiting, logging...)
#
access_log /var/log/nginx/warehouse_api.log main;
# URI routing
#
location /api/warehouse/inventory {
proxy_pass http://warehouse_inventory;
}
location /api/warehouse/pricing {
proxy_pass http://warehouse_pricing;
}
return 404; # Catch-all
}
响应错误
HTTP API 和基于浏览器的流量之间的一个关键区别是如何将错误传递给客户端。当 NGINX 部署为 API 网关时,我们将其配置为以最适合 API 客户端的方式返回错误。
顶层 API 网关配置包含了定义如何处理错误响应的部分。
# Error responses
error_page 404 = @400; # Treat invalid paths as bad requests
proxy_intercept_errors on; # Do not send backend errors to client
include api_json_errors.conf; # API client-friendly JSON errors
default_type application/json; # If no content-type, assume JSON
第 23 行的 error_page 指令定义了当请求与任何 API 定义都不匹配时,NGINX 返回 400
(Bad
Request
) 错误,而不是默认的 404
(Not
Found
) 错误。此(可选)行为要求 API 客户端仅发出 API 文档中包含的有效 URI 的请求,并防止未经授权的客户端发现通过 API 网关发布的 API 的 URI 结构。
第 24 行涉及后端 service 本身产生的错误。未处理的后端 service 的响应异常可能包含堆栈跟踪或其他我们不想发送给客户端的敏感数据。此配置可向客户端发送标准化错误响应,进一步增加了防护级别。
标准化错误响应的完整列表在第 25 行的 include
指令引用的单独配置文件中定义,其中的前几行如下所示。如果首选是 JSON 以外的格式,则可以修改此文件,将 api_gateway.conf 第 26 行的 default_type 值更改为匹配值。您还可以在每个 API 的策略部分添加一个单独的 include
指令,以引用不同的错误响应文件,这些文件会覆盖全局响应。
error_page 400 = @400;
location @400 { return 400 '{"status":400,"message":"Bad request"}\n'; }
error_page 401 = @401;
location @401 { return 401 '{"status":401,"message":"Unauthorized"}\n'; }
error_page 403 = @403;
location @403 { return 403 '{"status":403,"message":"Forbidden"}\n'; }
error_page 404 = @404;
location @404 { return 404 '{"status":404,"message":"Resource not found"}\n'; }
有了此配置,对无效 URI 的客户端请求将收到以下响应。
$ curl -i https://api.example.com/foo
HTTP/1.1 400 Bad Request
Server: nginx/1.19.5
Content-Type: application/json
Content-Length: 39
Connection: keep-alive
{"status":400,"message":"Bad request"}
实施认证
不通过某种形式的身份认证就发布 API 的情况较为罕见。NGINX 提供了多种方法来保护 API 和认证 API 客户端。要了解同样适用于常规 HTTP 请求的方法,请参阅基于 IP 地址的访问控制列表 (ACL)、数字证书身份认证和 HTTP basic 认证的文档。此处,我们重点介绍适用 API 的身份验证方法。
API 密钥认证
API 密钥是客户端和 API 网关的共享密钥。API 密钥本质上是一个作为长期凭证发给 API 客户端的长而复杂的密码。创建 API 密钥很简单 —— 只需像本例中那样编码产生一个随机数。
$ openssl rand -base64 18
7B5zIqmRGXmrJTFmKa99vcit
在顶层 API 网关配置文件 api_gateway.conf 的第 2 行,我们添加了一个名为 api_keys.conf 的文件,其中包含每个 API 客户端的 API 密钥,并由客户端名称或其他描述加以标识。以下是该文件的内容:
map $http_apikey $api_client_name {
default "";
"7B5zIqmRGXmrJTFmKa99vcit" "client_one";
"QzVV6y1EmQFbbxOfRCwyJs35" "client_two";
"mGcjH8Fv6U9y3BVF9H3Ypb9T" "client_six";
}
API 密钥在 map 块中定义。map
指令有两个参数。第一个参数定义在何处查找 API 密钥,本例中是在客户端请求的 apikey
HTTP 包头中,该包头于 $http_apikey
变量中捕获。第二个参数创建一个新变量 ($api_client_name
),并将其设置为第一个参数与密钥匹配行的第二个参数的值。
例如,当客户端请求中带有 API 密钥 7B5zIqmRGXmrJTFmKa99vcit
时,$api_client_name
变量设置为 client_one
。此变量可用于检查经过验证的客户端并包含在日志条目中以进行更详细的审核。Map
块的格式非常简单,容易集成到从已有凭证存储生成 api_keys.conf 文件的自动化工作流中。
此处,我们通过修改 “宽泛” 配置 (warehouse_api_simple.conf),在策略部分添加一个 auth_request 指令(将身份验证决策委托给指定 location),从而启用 API 密钥身份验证。
# 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/pricing {
proxy_pass http://warehouse_pricing;
}
return 404; # Catch-all
}
例如,通过 auth_request
指令(第 7 行),我们可以让外部身份认证服务器(例如 OAuth 2.0 token introspection)处理身份认证。在此示例中,我们将验证 API 密钥的逻辑添加到顶层 API 网关配置文件中,其形式为以下名为 /_validate_apikey 的 location
块。
# API key validation
location = /_validate_apikey {
internal;
if ($http_apikey = "") {
return 401; # Unauthorized
}
if ($api_client_name = "") {
return 403; # Forbidden
}
return 204; # OK (no content)
}
第 30 行的 internal 指令意味着外部客户端不能直接访问此位置(只能由 auth_request
访问)。客户端应在 apikey
HTTP 包头中显示其 API 密钥。如果此标头丢失或为空(第 32 行),我们将发送 401
(Unauthorized)
响应,告知客户端需要进行身份验证。第 35 行处理 API 密钥与 map
块中的任何密钥都不匹配的情况 —— 在这种情况下,api_keys.conf 中第 2 行的 default
参数将 $api_client_name
设置为空字符串,我们将发送 403
(Forbidden)
响应,告诉客户端身份验证失败。如果这些条件都不匹配,则 API 密钥有效并且该 location 返回 204
(No
Content)
响应。
有了此配置,Warehouse API 现在实现了 API 密钥身份认证。
$ curl https://api.example.com/api/warehouse/pricing/item001
{"status":401,"message":"Unauthorized"}
$ curl -H "apikey: thisIsInvalid" https://api.example.com/api/warehouse/pricing/item001
{"status":403,"message":"Forbidden"}
$ 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 相关的技术干货、互动问答、系列课程、活动资源?
请前往 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