八步部署NGINX Plus API网关

4,863 阅读9分钟

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~

本文来自云+社区翻译社,作者ArrayZoneYour

Nginx往往是构建微服务中必不可缺的一部分,从本文中你可以习得如何使用Nginx作为API网关。

HTTP API是现代应用架构的核心。HTTP协议使开发者可以更快地构建应用并使应用的维护变得更加容易。HTTP API提供了一套通用的接口,这使得在任意的应用规模下,我们都可以借助HTTP API从一个基本的微服务开始构建出一个具有完备功能的整体。借助HTTP,普通的web应用程序也可以在规模巨大的互联网上提供高性能、高可用的API。

如果你还不理解API网关对微服务应用的重要性,可以参阅Building Microservices: Using an API Gateway

作为领先的高性能、轻量级反向代理和负载均衡器解决方案,NGINX Plus具有处理API流量所需的高级HTTP处理能力。这使得NGINX Plus成为构建API网关的理想平台。在本文中,我们将使用一些常见的API网关为例展示如何配置NGINX Plus来以高效、可扩展、易维护的方式处理它们。最后我们会得到一套可作为生产环境部署基础的完整配置。

注:除特殊注明外,本文中所有的配置同时适用于NGINX和NGINX Plus。

样例API简介(以仓储背景为例)

API网关的主要功能是为不同的API分别提供单独,一致的入口点,它的实现与后端的实现与部署方式无关。实际场景中,往往不是所有的API都是以微服务的方式实现的。我们的API网关需要同时管理现有的API、巨无霸式的API(monoliths, 对与微服务相对的庞然大物的戏称)以及开始局部切换为微服务的应用等等。

在本文中,我们假想一个库存管理的API(WareHouse API)为例进行说明。我们使用实例的配置代码来说明不同的用例。我们假设的API是一个RESTful API,它接受JSON请求并生成JSON数据响应请求。虽然我们本文中是以RESTful API为例进行讲解,但是NGINX Plus作为API网关部署时并不要求或者限制JSON的使用;NGINX Plus本身并不知道API使用的架构或者数据格式。

WareHouse API 作为一组独立的微服务之一被实现并作为一个单独的API进行发布。其下的inventory 和 pricing 资源分别作为单独的服务集成并部署在不同的后端上。由此可以画出如下的API路径结构:

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

举例来说,如果我们想获得仓库的库存信息,则需要通过客户端发送一个 HTTP GET请求到/api/warehouse/inventory

img

组织NGINX的配置文件

我们使用NGINX Plus作为API网关的好处是它可以同时扮演反向代理、负载均衡器以及现有HTTP流量所需的web服务器这三个角色。如果NGINX Plus已经是你的应用交付栈的一部分,那么你不需要再用它部署一个单独的API网关。不过,API网关预期的默认行为与基于浏览器的流量所期望的默认行为不同,因此我们需要将API网关配置与现存(未来)的基于浏览器所需的流量对应的配置文件分来。

为了实现上述需求,我们为配置文件创建了以下目录结构来支持多用途的NGINX Plus实例,这也为通过CI / CD 管道自动配置并部署提供了便利。

etc/
	└── nginx/
  		  ├── api_conf.d/ ....................................... API配置的子目录
        │ └── warehouse_api.conf ...... Warehouse API 的定义及配置
        ├── api_backends.conf ..................... 后端服务配置 (upstreams)
        ├── api_gateway.conf ........................ API网关服务器的顶级配置
        ├── api_json_errors.conf ............ JSON格式的HTTP错误响应配置
        ├── conf.d/
        │ ├── ...
        │ └── existing_apps.conf
        └── nginx.conf

API网关配置的目录和文件名都加了**api_**前缀。上面的每个目录和文件都对应着API网关的不同功能和特性,我们在下面会逐个详细解释。

定义API网关的顶级配置

NGINX读取配置将从主配置文件nginx.conf开始。为了读取API网关配置,我们需要在nginx.confhttp块中添加一条指令来引用包含网关配置的文件api_gateway.conf (大概在28行附近)。从文件内容中我们可以看到nginx.conf中默认从conf.d子目录中读取基于浏览器的HTTP配置。本文中将广泛使用include命令来提高可读性并实现部分配置的自动化。

    include /etc/nginx/api_gateway.conf; # 所有的API网关配置
    include /etc/nginx/conf.d/*.conf;    # 正常的web流量配置

api_gateway.conf文件定义了将NGINX Plus作为API网关暴露给客户端的虚拟服务器的配置。该配置将暴露所有由API网关发布的API,入口位于https://api.example.com/,用TLS协议加密保护。注意这里使用的配置文件是针对HTTPS的——并没有使用明文传输的HTTP。这代表着我们默认并要求API客户端知道正确的入口点并使用HTTPS连接。

log_format api_main '$remote_addr - $remote_user [$time_local] "$request"'
                    '$status $body_bytes_sent "$http_referer" "$http_user_agent"'
                    '"$http_x_forwarded_for" "$api_name"';
include api_backends.conf;
include api_keys.conf;
server {
    set $api_name -; # Start with an undefined API name, each API will update this value
    access_log /var/log/nginx/api_access.log api_main; # Each API may also log to a separate file
    listen 443 ssl;
    server_name api.example.com;
    # TLS 配置
    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.1 TLSv1.2;
    # API 定义, 每个文件对应一个
    include api_conf.d/*.conf;
    # 错误响应
    error_page 404 = @400;         # 处理非法URI路径的请求
    proxy_intercept_errors on;     # 不将后端的错误消息发送给客户端
    include api_json_errors.conf;  # 定义返回给客户端的JSON响应数据
    default_type application/json; # 如果不指定 content-type 则默认为 JSON
}

以上配置是静态的,表现在每个独立API的细节以及响应的后端服务是通过include命令引用相应的文件实现的。上面文件的最后四行负责处理默认的日志输出以及错误处理。我们将在后面的 错误响应 一节中单独讨论。

单服务 vs. 微服务 API后端

一些API可以通过单个后端实现,但是出于弹性或者负载均衡等原因,我们通常期望有不止一个后端。通过微服务的API,我们可以为每个服务定义单独的后端,将他们组合在一起就形成了完整的API。在本文中,我们的仓储API被部署为两个独立的服务,每一个都有多个后端。

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服务均在api_backends.conf中被定义。这里我们在每个块中使用了多个IP地址-端口对来指示API代码的部署位置,我们也可以使用主机名来替换IP地址。NGINX Plus 的订阅用户还可以使用动态的DNS负载均衡功能自动地将新的后端添加至在线运行配置。

定义Warehouse API

这部分配置首先定义了Warehouse API的有效URI,然后定义了处理Warehouse API请求所用的通用策略。

# API 定义
#
location /api/warehouse/inventory {
    set $upstream warehouse_inventory;
    rewrite ^ /_warehouse last;
}
location /api/warehouse/pricing {
    set $upstream warehouse_pricing;
    rewrite ^ /_warehouse last;
}
# 策略
#
location = /_warehouse {
    internal;
    set $api_name "Warehouse";
    # 在这里配置相应的策略 (认证, 限速, 日志记录, ...)
    proxy_pass http://$upstream$request_uri;
}

Warehouse API 通过一系列配置块来定义。NGINX Plus具有灵活和高效的系统,这使得它可以将请求的URI与相应的配置块匹配。一般来说请求会通过具体的路径前缀进行匹配,location指令的顺序并不重要。在上面的配置中我们在第三行和第八行定义了两个路径前缀。在每个配置中,$upstream变量被设定为分别代表 inventory 和 pricing 的后端API服务。

此处这样配置的目的是将API的定义与API的交付逻辑分离。为了实现这一目标,我们尽量减少了API定义部分的配置内容。当我们为每个 location 确定了合适的 upstream 组之后,可以使用指令来查找相应的API策略。

img

rewrite指令的结果是NGINX Plus搜索开头为**/_warehouse**的URI对应的 location 块。上面的配置中使用了 = 修饰符来进行精确匹配,这提升了处理的速度。

在这个阶段,我们的策略块内容非常简单。在配置中的 iternal 意味着客户端不能直接向它发出请求。$api_name变量被重新定义为匹配API的名称,以便它可以在日志文件中正常显示。最后请求会通过使用 $request_uri 变量(包含未修改的原始请求URI)代理至API定义部分中指定的 upstreame 组。

API的 宽松定义 vs. 精确定义

API的定义有两种方法——宽松的或者精确的。每个API最适合的方法取决于API的安全要求以及后端服务是否需要处理无效的URI。

warehouse_api.simple.conf文件中,我们使用了宽松的方式来定义Warehouse API。这意味着任何前缀满足要求的URI都会被代理到相应的后端服务,即以下URI的API请求都会被作为有效URI进行处理:

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

如果我们只需要考虑将每个请求代理到正确的后端服务,那么宽松的定义可以提供最快的处理速度和最紧凑的配置。相对地,使用精确的定义方法可以通过明确定义每个可用API资源的URI路径来了解API的完整URI空间。Warehouse API 的下列配置结合使用完全匹配 ( = ) 和正则表达式 ( ~ ) 实现了对每个URI的精确匹配。

location = /api/warehouse/inventory { # Complete inventory
    set $upstream inventory_service;
    rewrite ^ /_warehouse last;
}
location ~ ^/api/warehouse/inventory/shelf/[^/]*$ { # Shelf inventory
    set $upstream inventory_service;
    rewrite ^ /_warehouse last;
}
location ~ ^/api/warehouse/inventory/shelf/[^/]*/box/[^/]*$ { # Box on shelf
    set $upstream inventory_service;
    rewrite ^ /_warehouse last;
}
location ~ ^/api/warehouse/pricing/[^/]*$ { # Price for specific item
    set $upstream pricing_service;
    rewrite ^ /_warehouse last;
}

上面的配置虽然啰嗦一点,但是更准确地描述了后端服务实现的资源。这可以使后端服务免受恶意用户请求的影响,但是会增加额外的开销来处理正则表达式的匹配。在这种配置下,NGINX Plus会接受部分URI,其余的会被视为无效而被拒绝:

img
匹配示例

使用精确的API定义可以利用现有的API文档格式驱动API网关的配置,使OpenAPI规范(过去称为Swagger)下的NGINX Plus API定义自动化。本文配套提供了相应的示例脚本

重写客户端请求

随着API的发展,有时出现的突发情况或变化要求更新客户端的请求。一个典型的例子就是原有的API资源被重命名或者移除。与web浏览器不同,API网关并不能向客户端发送带有API新的命名的重定向。不过幸运的是,我们可以通过重写客户端请求来解决这个问题。

在下面的代码中,我们可以看到在第三行的位置,pricing服务之前是作为inventory服务的一部分实现的。所以现在我们使用rewrite指令来将旧的pricing资源请求切换至了对新的pricing资源的请求。

# 重写规则
#
rewrite ^/api/warehouse/inventory/item/price/(.*)  /api/warehouse/pricing/$1;
# API 定义
#
location /api/warehouse/inventory {
    set $upstream inventory_service;
    rewrite ^(.*)$ /_warehouse$1 last;
}
location /api/warehouse/pricing {
    set $upstream pricing_service;
    rewrite ^(.*) /_warehouse$1 last;
}
# 处理策略
#
location /_warehouse {
    internal;
    set $api_name "Warehouse";
    # 在这里配置相应的策略 (认证, 限速, 日志记录, ...)
    rewrite ^/_warehouse/(.*)$ /$1 break; # 移除 /_warehouse 前缀
    proxy_pass http://$upstream;          # 代理重写后的URI
}

不过使用重写URI也意味着在上面代码的倒数第二行我们处理代理请求的时候不能再使用$request_uri变量(像warehouse_api_simple.conf的第21行的做法一样)。所以我们需要在上述代码的第9行和第14行的位置使用不同的rewrite指令之后将URI移交给策略部分的代码块进行处理。

img

错误响应

基于HTTP API和浏览器的流量之间的一个关键区别是错误传递给客户端的方式。当我们配置NGINX Plus作为API网关时,我们将其配置其以最适合API客户端的方式返回错误信息。

  # 错误响应
    error_page 404 = @400;         # 处理非法URI路径的请求
    proxy_intercept_errors on;     # 不将后端的错误消息发送给客户端
    include api_json_errors.conf;  # 定义返回给客户端的JSON响应数据
    default_type application/json; # 如果不指定 content-type 则默认为 JSON

上面的代码展示了我们在顶层的API网关中关于错误响应的配置。

由于上面第二行的配置,当请求不能够匹配到任何的API定义时,我们将返回该行定义的错误而不是NGINX Plus默认的错误响应给客户端。这个可选的行为要求客户端按照满足API文档规范的方式进行请求,这避免了未经授权的用户通过API网关发现API的URI结构。

proxy_interceprt_errors指的是后端服务生成的错误信息。原始的错误信息可能包含着错误的堆栈信息或者其他以及一些其他我们不希望客户端看到的敏感信息。打开这一配置之后,我们将错误信息标准化之后再发送给客户端,从而进一步提升信息的安全级别。

再下一行,我们通过include指令引入了错误响应的完整列表,下面展示了其中的前几行。如果你想采用JSON以外的其他错误格式,那么你可以修改最后一行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.13.10
Content-Type: application/json
Content-Length: 39
Connection: keep-alive
{"status":400,"message":"Bad request"}

身份认证集成

在发布API时,我们通常都会通过身份认证来保护它们。NGINX Plus提供了几种方法来保护API以及验证API客户端。相关的具体信息可以参阅NGINX官方文档中的IP address‑based access control listsdigital certificate authentication以及HTTP Basic authentication部分。在本文中,我们将专注于适用于API的认证方法。

API秘钥认证

API秘钥是客户端和API网关同时掌握其内容的共享秘钥。其本质就是一个长度很长的复杂密码,它通常作为一个长期凭证提供给API客户端。创建API秘钥的操作十分简单,你只需要像下面一样编码一个随机数即可。

$ openssl rand -base64 18 7B5zIqmRGXmrJTFmKa99vcit

现在回到顶层的API网关配置文件api_gateway.conf,可以看到第6行我们include了一个名为api_key.conf的文件,它包含着每个API客户端的API秘钥信息以及相匹配的客户端名称或相关描述。

map $http_apikey $api_client_name {
    default "";
    "7B5zIqmRGXmrJTFmKa99vcit" "client_one";
    "QzVV6y1EmQFbbxOfRCwyJs35" "client_two";
    "mGcjH8Fv6U9y3BVF9H3Ypb9T" "client_six";
}

可以看到API秘钥被定义在上面展示的代码块当中。其中的map指令接受了两个参数。第一个参数定义了寻找API秘钥的位置,这里我们通过获取客户端HTTP请求头中的apikey作为变量$http_api_key接收。第二个参数创建了一个新变量$api_client_name并且将其与第一个参数即同行的API秘钥相匹配。

此时,如果客户端提供了API秘钥7B5zIqmRGXmrJTFmKa99vcit是,变量$api_client_name会被设置为client_one。这个变量可以用于检验通过身份验证的客户端以及对日志的进一步审计。

可以看到map块的格式非常简单,这使得我们可以很容易地将api_keys.conf的生成集成到自动化的工作流当中。之后可以在API的策略块中完成API秘钥的校验逻辑。

# 策略块
#
location = /_warehouse {
    internal;
    set $api_name "Warehouse";  
    if ($http_apikey = "") {
        return 401; # Unauthorized (please authenticate)
    }
    if ($api_client_name = "") {
        return 403; # Forbidden (invalid API key)
    }
    proxy_pass http://$upstream$request_uri;
}

我们希望发送请求的客户端都在它们的HTTP头部中指定apikey内容为客户端持有的API秘钥。如果没有HTTP头信息或者其中没有apikey,我们将返回给客户端401状态码要求其完成认证。如果客户端发送的API秘钥不存在于api_keys.conf当中,$api_client_name会被设置为默认值即空字符串——此时我们将返回403状态码来告诉客户端其认证无效。

完成以上配置之后,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 Token ( JWT )已经越来越广泛地被应用于API认证。不过要注意的是原生JWT支持是NGINX Plus才有的特性。关于如何启用JWT支持可以参阅Authenticating API Clients with JWT and NGINX Plus

总结

本文是部署NIGNX Plus作为API网关系列文章中的第一篇。本文中使用到的所有文件可以在我们的GitHub Gist repo上下载或查看。在本系列的下一篇文章中我们将探讨更高级的用例以保护后端服务免受恶意或者非法操作的用户的侵害。


问答

如何用nginx编写url重写?

相关阅读

如何用Nginx快速搭建一个安全的微服务架构

Nginx 原理解析和配置摘要

使用API网关构建微服务


此文已由作者授权腾讯云+社区发布,原文链接:https://cloud.tencent.com/developer/article/1149103?fromSource=waitui

欢迎大家前往腾讯云+社区或关注云加社区微信公众号(QcloudCommunity),第一时间获取更多海量技术实践干货哦~