部署Nginx Plus作為API網關:Nginx
了解著名的Nginx服務器(微服務必不可少的東西)如何用作API網關。
現代應用程序體系結構的核心是HTTP API。 HTTP使應用程序能夠快速構建并輕松維護。無論應用程序的規模如何,HTTP API都提供了一個通用接口,從單用途微服務到無所不包的整體。通過使用HTTP,支持超大規模Internet屬性的Web應用程序交付的進步也可用于提供可靠和高性能的API交付。
有關API網關對微服務應用程序重要性的精彩介紹,請參閱我們博客上的構建微服務:使用API網關。
作為領先的高性能,輕量級反向代理和負載均衡器,NGINX Plus具有處理API流量所需的高級HTTP處理功能。這使得NGINX Plus成為構建API網關的理想平臺。在這篇博文中,我們描述了許多常見的API網關用例,并展示了如何配置NGINX Plus以便以高效,可擴展且易于維護的方式處理它們。我們描述了一個完整的配置,它可以構成生產部署的基礎。
注意:除非另有說明,否則本文中的所有信息均適用于NGINX Plus和NGINX開源。
介紹Warehouse API
API網關的主要功能是為多個API提供單一,一致的入口點,無論它們在后端如何實現或部署。并非所有API都是微服務應用程序。我們的API網關需要管理現有的API,單塊和正在部分過渡到微服務的應用程序。
在這篇博文中,我們引用了一個假設的庫存管理API,即“倉庫API”。我們使用示例配置代碼來說明不同的用例。 Warehouse API是一個RESTful API,它使用JSON請求并生成JSON響應。但是,當部署為API網關時,使用JSON不是NGINX Plus的限制或要求; NGINX Plus與API本身使用的架構風格和數據格式無關。
Warehouse API實現為離散微服務的集合,并作為單個API發布。庫存和定價資源作為單獨的服務實施,并部署到不同的后端。所以API的路徑結構是:
- api └── warehouse
- ├── inventory
- └── pricing
例如,要查詢當前倉庫庫存,客戶端應用程序會向/ api / warehouse / inventory發出HTTP GET請求。
組織NGINX配置
使用NGINX Plus作為API網關的一個優點是,它可以執行該角色,同時充當現有HTTP流量的反向代理,負載平衡器和Web服務器。如果NGINX Plus已經是應用程序交付堆棧的一部分,那么通常不需要部署單獨的API網關。但是,API網關所期望的某些默認行為與基于瀏覽器的流量的預期不同。出于這個原因,我們將API網關配置與基于瀏覽器的流量的任何現有(或未來)配置分開。
為實現這種分離,我們創建了一個支持多用途NGINX Plus實例的配置布局,并為通過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網關的不同特性和功能,并在下面詳細說明。
定義頂級API網關
所有NGINX配置都以主配置文件nginx.conf開頭。要讀入API網關配置,我們在nginx.conf的http塊中添加一個指令,該指令引用包含網關配置的文件api_gateway.conf(下面的第28行)。請注意,默認的nginx.conf文件使用include偽指令從conf.d子目錄中引入基于瀏覽器的HTTP配置(第29行)。本博文廣泛使用include指令來提高可讀性并實現配置某些部分的自動化。
- include /etc/nginx/api_gateway.conf; # All API gateway configuration
- include /etc/nginx/conf.d/*.conf; # Regular web traffic
api_gateway.conf文件定義了將NGINX Plus公開為客戶端的API網關的虛擬服務器。此配置公開API網關在單個入口點https://api.example.com/(第13行)發布的所有API,受第16到21行配置的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 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.1 TLSv1.2;
- # 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
- }
此配置是靜態的 - 各個API及其后端服務的詳細信息在第24行的include偽指令引用的文件中指定。第27到30行處理日志記錄默認值和錯誤處理,并在響應中討論錯誤部分如下。
單服務與微服務API后端
一些API可以在單個后端實現,但是出于彈性或負載平衡的原因,我們通常期望存在多個API。使用微服務API,我們為每個服務定義單獨的后端;它們一起作為完整的API。在這里,我們的Warehouse 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代碼的部署位置,但也可以使用主機名。 NGINX Plus訂戶還可以利用動態DNS負載平衡,自動將新后端添加到運行時配置中。
定義Warehouse API
配置的這一部分首先定義Warehouse API的有效URI,然后定義用于處理對Warehouse API的請求的公共策略。
- # API definition
- #
- location /api/warehouse/inventory {
- set $upstream warehouse_inventory;
- rewrite ^ /_warehouse last;
- }
- location /api/warehouse/pricing {
- set $upstream warehouse_pricing;
- rewrite ^ /_warehouse last;
- }
- # Policy section
- #
- location = /_warehouse {
- internal;
- set $api_name "Warehouse";
- # Policy configuration here (authentication, rate limiting, logging, more...)
- proxy_pass http://$upstream$request_uri;
- }
Warehouse API定義了許多塊。 NGINX Plus具有高效靈活的系統,可將請求URI與配置的一部分進行匹配。通常,請求由最具體的路徑前綴匹配,并且位置指令的順序并不重要。這里,在第3行和第8行,我們定義了兩個路徑前綴。在每種情況下,$ upstream變量都設置為上游塊的名稱,該上游塊分別代表庫存和定價服務的后端API服務。
此配置的目標是將API定義與管理API交付方式的策略分開。為此,我們最小化了API定義部分中顯示的配置。在為每個位置確定適當的上游組之后,我們停止處理并使用指令來查找API的策略(第10行)。

使用重寫指令將處理移至API策略部分
重寫指令的結果是NGINX Plus搜索匹配以/ _warehouse開頭的URI的位置塊。第15行的位置塊使用=修飾符執行完全匹配,從而加快處理速度。
在這個階段,我們的政策部分非常簡單。位置塊本身標記為第16行,這意味著客戶端無法直接向它發出請求。重新定義$ api_name變量以匹配API的名稱,以便它在日志文件中正確顯示。最后,請求被代理到API定義部分中指定的上游組,使用$ request_uri變量 - 其中包含原始請求URI,未經修改。
選擇廣泛或者精確的API定義
API定義有兩種方法 - 廣泛而精確。每種API最合適的方法取決于API的安全要求以及后端服務是否需要處理無效的URI。
在warehouse_api_simple.conf中,我們通過在第3行和第8行定義URI前綴來使用Warehouse API的廣泛方法。這意味著以任一前綴開頭的任何URI都代理到相應的后端服務。使用基于前綴的位置匹配,對以下URI的API請求都是有效的:
- /api/warehouse/inventory
- /api/warehouse/inventory/
- /api/warehouse/inventory/foo
- /api/warehouse/inventoryfoo
- /api/warehouse/inventoryfoo/bar/
如果唯一的考慮是將每個請求代理到正確的后端服務,則廣泛的方法提供最快的處理和最緊湊的配置。另一方面,精確的方法使API網關能夠通過顯式定義每個可用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并拒絕其他URI無效:

使用精確的API定義,現有的API文檔格式可以驅動API網關的配置。可以從OpenAPI規范(以前稱為Swagger)自動化NGINX Plus API定義。此博客文章的Gists中提供了用于此目的的示例腳本。
重寫客戶請求
隨著API的發展,有時會發生需要更新客戶端的重大更改。一個這樣的示例是重命名或移動API資源。與Web瀏覽器不同,API網關無法向其客戶端發送命名新位置的重定向(代碼301)。幸運的是,當修改API客戶端不切實際時,我們可以動態地重寫客戶端請求。
在下面的示例中,我們可以在第3行看到定價服務以前是作為庫存服務的一部分實現的:rewrite指令將對舊定價資源的請求轉換為新的定價服務。
- # Rewrite rules
- #
- rewrite ^/api/warehouse/inventory/item/price/(.*) /api/warehouse/pricing/$1;
- # API definition
- #
- location /api/warehouse/inventory {
- set $upstream inventory_service;
- rewrite ^(.*)$ /_warehouse$1 last;
- }
- location /api/warehouse/pricing {
- set $upstream pricing_service;
- rewrite ^(.*) /_warehouse$1 last;
- }
- # Policy section
- #
- location /_warehouse {
- internal;
- set $api_name "Warehouse";
- # Policy configuration here (authentication, rate limiting, logging, more...)
- rewrite ^/_warehouse/(.*)$ /$1 break; # Remove /_warehouse prefix
- proxy_pass http://$upstream; # Proxy the rewritten URI
- }
動態重寫URI意味著當我們最終在第26行代理請求時,我們不能再使用$ request_uri變量(正如我們在warehouse_api_simple.conf的第21行所做的那樣)。這意味著我們需要在API定義部分的第9行和第14行使用稍微不同的重寫指令,以便在處理切換到策略部分時保留URI。

回應錯誤
HTTP API和基于瀏覽器的流量之間的主要區別之一是如何將錯誤傳達給客戶端。當NGINX Plus作為API網關部署時,我們將其配置為以最適合API客戶端的方式返回錯誤。
- # 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
頂級API網關配置包括一個定義如何處理錯誤響應的部分。
第27行的指令指定當請求與任何API定義都不匹配時,NGINX Plus會返回錯誤而不是默認錯誤。此(可選)行為要求API客戶端僅向API文檔中包含的有效URI發出請求,并防止未經授權的客戶端發現通過API網關發布的API的URI結構。
第28行指的是后端服務本身產生的錯誤。未處理的異常可能包含我們不希望發送到客戶端的堆棧跟蹤或其他敏感數據。此配置通過向客戶端發送標準化錯誤來進一步提供保護。
完整的錯誤響應列表在第29行的include偽指令引用的單獨配置文件中定義,其前幾行如下所示。如果首選不同的錯誤格式,并且通過更改第30行上的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客戶端的方法。有關基于IP地址的訪問控制列表(ACL),數字證書身份驗證和HTTP基本身份驗證的信息,請參閱文檔。在這里,我們專注于API特定的身份驗證方法。
API密鑰身份驗證
API密鑰是客戶端和API網關已知的共享密鑰。它們本質上是作為長期憑證發布給API客戶端的長而復雜的密碼。創建API密鑰很簡單 - 只需編碼一個隨機數,如本例所示。
- openssl rand -base64 18 7B5zIqmRGXmrJTFmKa99vcit
在頂級API網關配置文件api_gateway.conf的第6行,我們包含一個名為api_keys.conf的文件,其中包含每個API客戶端的API密鑰,由客戶端名稱或其他描述標識。
- map $http_apikey $api_client_name {
- default "";
- "7B5zIqmRGXmrJTFmKa99vcit" "client_one";
- "QzVV6y1EmQFbbxOfRCwyJs35" "client_two";
- "mGcjH8Fv6U9y3BVF9H3Ypb9T" "client_six";
- }
API密鑰在塊中定義。 map指令有兩個參數。第一個定義了API密鑰的位置,在本例中是在$ http_apikey變量中捕獲的客戶端請求的apikey HTTP頭。第二個參數創建一個新變量($ api_client_name)并將其設置為第一個參數與鍵匹配的行上的第二個參數的值。
例如,當客戶端提供API密鑰7B5zIqmRGXmrJTFmKa99vcit時,$ api_client_name變量設置為client_one。此變量可用于檢查經過身份驗證的客戶端,并包含在日志條目中以進行更詳細的審核。
地圖塊的格式很簡單,易于集成到自動化工作流程中,從現有的憑證存儲生成api_keys.conf文件。 API密鑰身份驗證由每個API的策略部分強制執行。
- # Policy section
- #
- 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;
- }
客戶端應在apikey HTTP頭中顯示其API密鑰。如果此標頭丟失或為空(第20行),我們發送401響應以告知客戶端需要進行身份驗證。第23行處理API鍵與地圖塊中的任何鍵都不匹配的情況 - 在這種情況下,api_keys.conf第2行的默認參數將$ 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令牌(JWT)越來越多地用于API身份驗證。原生JWT支持是NGINX Plus獨有的,可以在我們的博客上驗證JWT,如使用JWT和NGINX Plus驗證API客戶端中所述。