八步部署NGINX Plus API閘道器

騰訊雲加社群發表於2018-06-22

歡迎大家前往騰訊雲+社群,獲取更多騰訊海量技術實踐乾貨哦~

本文來自雲+社群翻譯社,作者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),第一時間獲取更多海量技術實踐乾貨哦~

相關文章