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(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.
我们使用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网关的不同功能和特性,我们在下面会逐个详细解释。
NGINX读取配置将从主配置文件nginx.conf开始。为了读取API网关配置,我们需要在nginx.conf中http
块中添加一条指令来引用包含网关配置的文件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
命令引用相应的文件实现的。上面文件的最后四行负责处理默认的日志输出以及错误处理。我们将在后面的 错误响应 一节中单独讨论。
一些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的有效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策略。
rewrite
指令的结果是NGINX Plus搜索开头为/_warehouse的URI对应的 location 块。上面的配置中使用了 = 修饰符来进行精确匹配,这提升了处理的速度。
在这个阶段,我们的策略块内容非常简单。在配置中的 iternal 意味着客户端不能直接向它发出请求。$api_name
变量被重新定义为匹配API的名称,以便它可以在日志文件中正常显示。最后请求会通过使用 $request_uri 变量(包含未修改的原始请求URI)代理至API定义部分中指定的 upstreame 组。
API的定义有两种方法——宽松的或者精确的。每个API最适合的方法取决于API的安全要求以及后端服务是否需要处理无效的URI。
在warehouse_api.simple.conf文件中,我们使用了宽松的方式来定义Warehouse API。这意味着任何前缀满足要求的URI都会被代理到相应的后端服务,即以下URI的API请求都会被作为有效URI进行处理:
如果我们只需要考虑将每个请求代理到正确的后端服务,那么宽松的定义可以提供最快的处理速度和最紧凑的配置。相对地,使用精确的定义方法可以通过明确定义每个可用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,其余的会被视为无效而被拒绝:
使用精确的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移交给策略部分的代码块进行处理。
基于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 lists,digital certificate authentication以及HTTP Basic authentication部分。在本文中,我们将专注于适用于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}
现在,JSON Web Token ( JWT )已经越来越广泛地被应用于API认证。不过要注意的是原生JWT支持是NGINX Plus才有的特性。关于如何启用JWT支持可以参阅Authenticating API Clients with JWT and NGINX Plus。
本文是部署NIGNX Plus作为API网关系列文章中的第一篇。本文中使用到的所有文件可以在我们的GitHub Gist repo上下载或查看。在本系列的下一篇文章中我们将探讨更高级的用例以保护后端服务免受恶意或者非法操作的用户的侵害。
腾讯云分布式微服务来啦!
腾讯分布式微服务TSF围绕应用和微服务的PaaS平台,提供服务全生命周期管理能力和数据化运营支持,提供多维度应用、服务、机器的监控数据,助力服务性能优化;拥抱 Spring Cloud 开源社区技术更新和K8s容器化部署。