將 NGINX 部署為 API 閘道器,第 1 部分

語言: CN / TW / HK

原文作者:Liam Crilly of F5

原文連結:將 NGINX 部署為 API 閘道器,第 1 部分

轉載來源:NGINX 官方網站


本文是將 NGINX 開源版和 NGINX Plus 部署為 API 網關係列博文的第一篇。

  • 本文提供了幾個用例的詳細配置說明。文章最初發佈於 2018 年,現進行了更新,以反映 API 配置的當前最佳實踐——即使用巢狀的 location 塊來路由請求,而不是通過重寫規則。
  • 第 2 部分對這些用例進行了擴充套件,探討了一系列可用於保護生產環境中後端 API 服務的安全措施。
  • 第 3 部分解釋瞭如何將 NGINX 開源版和 NGINX Plus 部署為 gRPC 服務的 API 閘道器。

:除非另有說明,否則本文中的所有資訊都適用於 NGINX 開源版和 NGINX Plus。為了便於閱讀,下文將 NGINX 開源版和 NGINX Plus 統稱為“NGINX”。

現代應用架構的核心是 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。此配置在單個入口點 http://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 http://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 http://api.example.com/api/warehouse/pricing/item001
{"status":401,"message":"Unauthorized"}
$ curl -H "apikey: thisIsInvalid" http://api.example.com/api/warehouse/pricing/item001
{"status":403,"message":"Forbidden"}
$ curl -H "apikey: 7B5zIqmRGXmrJTFmKa99vcit" http://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 開源社群: