將 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。此配置在單個入口點 https://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 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"}

實施認證

不通過某種形式的身份認證就發佈 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 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 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 開源社區: