将NGINX部署为API网关(第1部分)

NGINX | 2021年1月20日

这是我们关于将NGINX Open Source和NGINX Plus部署为API网关的系列博客文章中的第一篇:

  • 本文针对多个用例提供了详细的配置说明。本文最初发布于2018年,现已更新,以反映当前的API配置最佳实践,即使用嵌套的 location 块路由请求,而不再使用重写规则。
  • 第2部分在这些用例的基础上进一步展开,并介绍可用于保护生产环境中后端API服务并确保其安全的一系列防护措施。
  • 第3部分介绍如何将NGINX Open Source和NGINX Plus部署为gRPC服务的API网关。

注意:除非另有说明,本文中的所有信息均适用于NGINX Open Source和NGINX Plus。为便于阅读,本文其余部分将二者统称为“NGINX”。

HTTP API是现代应用架构的核心。HTTP有助于快速构建应用并简化维护。无论应用规模如何,从单一用途的微服务到涵盖所有功能的单体应用,HTTP API都能提供通用接口。借助HTTP,支持超大规模互联网服务的Web应用交付技术也可用于实现可靠的高性能API交付。

如需深入了解API网关对微服务应用的重要性,请参阅我们博客上的《构建微服务:使用API网关》。

作为一款广受认可的高性能轻量级反向代理和负载均衡器,NGINX具备处理API流量所需的高级HTTP处理能力,因此非常适合用于构建API网关。本篇博文将介绍多种常见的API网关用例,并说明如何配置NGINX,以高效、可扩展且易于维护的方式处理这些用例。文中还将介绍一套完整配置,可作为生产环境部署的基础。

Warehouse API简介

API网关的主要功能是为多个API提供统一且一致的入口,无论这些API在后端如何实现或部署。并非所有API都是微服务应用。我们的API网关需要管理现有API、单体应用,以及正在部分向微服务过渡的应用。

本文将使用一个假设的库存管理API(“Warehouse API”),并通过示例配置代码说明不同的用例。Warehouse API是一种RESTful API,可接收JSON请求并生成JSON响应。不过,将NGINX部署为API网关时,使用JSON既不是限制条件,也不是必要要求;无论API本身采用何种架构风格和数据格式,NGINX均可适用。

Warehouse API由一组独立的微服务实现,并作为单一API发布。库存资源和定价资源分别作为独立服务实现,并部署到不同的后端。因此,该API的路径结构如下:

api
└── warehouse
    ├── inventory
    └── pricing

例如,要查询当前仓库库存,客户端应用程序会发送HTTP GET请求至/api/warehouse/inventory。

多应用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网关配置,需要在include指令中添加到http块(位于nginx.conf)中,并引用包含网关配置的文件api_gateway.conf(见下方第28行)。请注意,默认的nginx.conf文件使用include指令从conf.d子目录中加载基于浏览器的HTTP配置(第29行)。本博文大量使用include指令,以提高可读性,并支持部分配置的自动化。

正在加载代码示例…

The api_gateway.conf文件定义了一个虚拟服务器,通过该服务器将NGINX作为API网关提供给客户端。此配置通过单一入口点公开API网关发布的所有API:https://api.example.com/(第9行)。该入口点按照第12至17行的配置受TLS保护。请注意,此配置仅使用HTTPS,不设明文HTTP侦听器。API客户端应知晓正确的入口点,并默认建立HTTPS连接。

此配置应保持静态。各个API及其后端服务的详细信息在第20行include指令引用的文件中指定。第23至26行用于错误处理,详情请参阅下文的“响应错误”。

正在加载代码示例…

单一服务与微服务API后端对比

某些API可能仅在单个后端实现,但出于弹性或负载均衡方面的考虑,通常会使用多个后端。对于微服务API,我们会为每项服务分别定义后端,这些后端共同构成完整的API。在此示例中,Warehouse API部署为两项独立服务,每项服务均有多个后端。

正在加载代码示例…

API网关发布的所有API所对应的后端API服务均在api_backends.conf中定义。此处,我们在每个upstream块中使用多个IP地址和端口对来指明API代码的部署位置,也可以使用主机名。NGINX Plus订阅用户还可以利用动态DNS负载均衡,将新的后端自动添加到运行时配置中。

定义Warehouse API

Warehouse API由嵌套配置中的多个location块定义,如以下示例所示。外层location块(/api/warehouse)用于标识基础路径,其下的嵌套位置指定路由到后端API服务的有效URI。使用外层块可以定义适用于整个API的通用策略(在本例中,即第6行的日志记录配置)。

正在加载代码示例…

NGINX提供了一套高效灵活的系统,可将请求URI与配置中的相应部分进行匹配。location指令的顺序并不重要,系统会选择最具体的匹配项。此处,第10行和第14行的嵌套location定义了两个比外层location块更具体的URI;每个嵌套块中的proxy_pass指令会将请求路由到相应的上游组。除非需要为某些URI提供更具体的策略,否则策略配置将从外层location继承。

任何与嵌套location均不匹配的URI都由外层location处理。外层location包含一个全匹配指令(第18行),会针对所有无效URI返回响应404 (Not Found)。

为API选择宽泛定义还是精确定义

API定义有两种方式:宽泛定义和精确定义。每个API适合采用哪种方式,取决于该API的安全要求,以及是否希望由后端服务处理无效URI。

在上面的warehouse_api_simple.conf中,我们对Warehouse API采用宽泛定义方式,在第10行和第14行定义URI前缀,使以其中任一前缀开头的URI都能被代理到相应的后端服务。采用这种基于前缀的宽泛位置匹配方式时,向以下URI发出的API请求均有效:

/api/warehouse/inventory
/api/warehouse/inventory/
/api/warehouse/inventory/foo
/api/warehouse/inventoryfoo
/api/warehouse/inventoryfoo/bar/

如果只需考虑将每个请求代理到正确的后端服务,宽泛方法可实现更快的处理速度和更精简的配置。另一方面,更精确的方法会明确指定每个可用API资源的URI路径,使API网关能够了解该API的完整URI空间。采用精确方法时,Warehouse API的以下URI路由配置结合使用精确匹配(=)和正则表达式(~),以定义每个有效URI。

正在加载代码示例…

此配置较为冗长,但能更准确地描述后端服务实现的资源。这样做可以保护后端服务免受格式错误的客户端请求影响,但会因正则表达式匹配而产生少量额外开销。采用此配置后,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定义。本博客文章的Gist中提供了用于此目的的示例脚本。

重写客户端请求以应对破坏性变更

随着API不断演进,有时必须进行会破坏严格向后兼容性的更改,并要求更新客户端。例如,重命名或移动API资源。与Web浏览器不同,API网关无法向客户端发送指明新位置的重定向(代码301 (Moved Permanently))。所幸,当修改API客户端并不切实时,可以即时重写客户端请求。

在以下示例中,我们采用与上文warehouse_api_simple.conf大致相同的方法,但此配置将替换旧版Warehouse API。在旧版中,定价服务作为库存服务的一部分实现。第3行的rewrite指令会将对旧定价资源的请求转换为对新定价服务的请求。

正在加载代码示例…

响应错误

HTTP API与基于浏览器的流量之间的一个关键区别,在于向客户端传达错误的方式。将NGINX部署为API网关时,我们会对其进行配置,使其以最适合API客户端的方式返回错误。

顶层API网关配置中包含一个用于定义错误响应处理方式的部分。

正在加载代码示例…

第23行的“error_page”指令规定,当请求与任何API定义均不匹配时,NGINX将返回400(Bad Request)错误,而不是默认的404(Not Found)错误。此行为为可选行为,要求API客户端仅向API文档中列出的有效URI发出请求,并防止未经授权的客户端发现通过API网关发布的API的URI结构。

第24行涉及后端服务自身生成的错误。未处理的异常可能包含堆栈跟踪或其他不应发送给客户端的敏感数据。此配置通过向客户端发送标准化错误响应,提供进一步保护。

标准化错误响应的完整列表在单独的配置文件中定义,第25行的include指令引用了该文件。下面显示了该文件开头的几行。如果希望使用JSON以外的错误格式,可以修改此文件,并相应更改default_type在api_gateway.conf第26行中的值。还可以在每个API的策略部分中分别添加include指令,以引用其他错误响应文件,并用其中的响应覆盖全局响应。

正在加载代码示例…

配置完成后,客户端请求无效URI时会收到以下响应。[terminal]$ 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"}[/terminal]

实施身份验证

发布API时,通常会采用某种身份验证机制加以保护。NGINX提供了多种方法来保护API并验证API客户端的身份。有关同样适用于常规HTTP请求的方法,请参阅以下文档:基于IP地址的访问控制列表(ACL)、数字证书身份验证和HTTP基本身份验证。本文重点介绍API专用的身份验证方法。

API密钥身份验证

API密钥是客户端和API网关共同知晓的共享密钥。API密钥本质上是向API客户端颁发的一个长且复杂的密码,用作长期凭据。创建API密钥很简单,只需像本例一样对随机数进行编码即可。

$ openssl rand -base64 187B5zIqmRGXmrJTFmKa99vcit

在顶层API网关配置文件的第2行,api_gateway.conf 中包含了一个名为api_keys.conf的文件。该文件包含每个API客户端的API密钥,并以客户端名称或其他描述作为标识。该文件的内容如下:

正在加载代码示例…

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指令,将身份验证决策委托给指定位置,从而启用API密钥身份验证。

正在加载代码示例…

例如,使用auth_request指令(第7行),可以由外部身份验证服务器处理身份验证,例如OAuth 2.0令牌内省。但在本例中,我们将API密钥验证逻辑添加到顶层API网关配置文件中,具体采用以下名为location的块:/_validate_apikey。

正在加载代码示例…

第30行的internal指令表示,外部客户端无法直接访问此location(只能通过auth_request访问)。客户端应在apikey HTTP标头中提供API密钥。如果此标头缺失或为空(第32行),我们会发送401 (未授权)响应,告知客户端需要进行身份验证。第35行处理API密钥与map块中的任何密钥均不匹配的情况。此时,api_keys.conf第2行的default参数会将$api_client_name设置为空字符串,我们会发送403 (禁止访问)响应,告知客户端身份验证失败。如果上述两种情况均不符合,则API密钥有效,该location会返回204 (无 内容)响应。

完成此配置后,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 令牌(JWT)越来越多地用于API身份验证。只有NGINX Plus提供原生JWT支持,可按照我们博客文章《使用JWT和NGINX Plus对API客户端进行身份验证》中的说明验证JWT。有关实现示例,请参阅第2部分的《控制对特定方法的访问》。

总结

本系列的首篇博客详细介绍了将NGINX Open Source和NGINX Plus部署为API网关的完整解决方案。您可以从我们的GitHub Gist代码库查看并下载本博客中讨论的全套文件。

查看本系列的其他文章:

  • 第2部分探讨了更高级的用例,帮助保护后端服务免受恶意客户端或行为不当的客户端侵害。
  • 第 3 部分介绍如何将NGINX部署为gRPC服务的API网关。

要试用NGINX Plus,请立即开始30天免费试用,或联系我们讨论您的使用场景。


分享

关于作者

相关博客文章

NGINX Plus R26 正式发布
NGINX | 2022/02/15

NGINX Plus R26 正式发布

NGINX Plus R26通过缓存JSON Web密钥集加快JWT验证,并强化TLS握手,从而提升安全性。NGINX JavaScript的新功能包括增强对异步函数的支持,以及实现WebCrypto API。

使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击
NGINX | 2022/02/10

使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击

gRPC具备开发人员设计和部署现代应用所需的速度与灵活性,但其开放性也使其极易受到第7层DoS攻击。NGINX App Protect DoS可保护gRPC应用免受此类破坏性攻击。

Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划
NGINX | 2021/09/13

Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划

了解选择错误的Ingress控制器可能带来的风险,以及如何从关键方面着手,让您的选择更好地适应未来需求。

NGINX Plus R19 正式发布
NGINX | 2019/09/03

NGINX Plus R19 正式发布

NGINX Plus R19扩展了监控功能,不仅提供更多指标,还新增了指标分析方式;增加试运行模式,用于测试限速效果;增强键值存储,以支持IP地址范围;并使限速更加动态。

NGINX现已正式加入F5
NGINX | 2019/05/09

NGINX现已正式加入F5

NGINX现已正式成为F5的一部分。Gus Robertson现任F5 NGINX业务部门负责人。他阐述了两家公司在人才、文化和愿景方面融合的价值,以及双方将如何携手增强NGINX的开源产品和商业产品。

使用NGINX和NGINX Plus实现高性能缓存
NGINX | 2016/08/24

使用NGINX和NGINX Plus实现高性能缓存

我将介绍NGINX在内容缓存和性能方面的工作原理,并分享一些控制NGINX内容缓存方式的技巧。