基于 OpenResty 的短信驗證碼平臺接口網(wǎng)關設計
來源:原創(chuàng) 時間:2017-10-13 瀏覽:0 次
本文講述基于 OpenResty 的短信驗證碼平臺接口關設計,主要談及接口網(wǎng)關的請求路由與安全認證(IP 與 URI 白名單、加解密與驗簽名流程等)這兩部分內容,其中涉及到的 Nginx、OpenResty 等相關內容會作簡單介紹。
談談基于 OpenResty 的接口網(wǎng)關設計
〇、前言
一、什么是接口網(wǎng)關
1.1 定位
1.2 功能
二、為什么需要接口網(wǎng)關
2.1 請求路由
2.2 安全認證
三、如何開發(fā)接口網(wǎng)關
3.2.1 兩層 HAProxy 代理
3.2.2 接口網(wǎng)關
3.2.3 架構總結
3.2.2.1 主流程設計
3.2.2.2 配置服務設計
3.2.2.3 安全服務設計
3.1.1 Nginx 簡介
3.1.1 OpenResty 簡介
3.1 Nginx 與 OpenResty 簡介
3.2 接口網(wǎng)關的架構
〇、前言
筆者曾參與開發(fā)兩個短信驗證碼平臺接口網(wǎng)關的項目,一個是基于 Tomcat 的應用提供的網(wǎng)關服務,另一個是基于 OpenResty 的 Nginx 應用提供的網(wǎng)關服務。經(jīng)過兩個網(wǎng)關項目的開發(fā),筆者在接口網(wǎng)關開發(fā)方面稍微積累了一些經(jīng)驗,故在此把這些經(jīng)驗分享出來一起交流學習。由于基于 OpenResty 的 Nginx 網(wǎng)關普遍被認為是更優(yōu)的方案,故本文主要針對基于 OpenResty 的 Nginx 網(wǎng)關進行講述。當然,由于不同的并發(fā)數(shù)量級,不同的業(yè)務場景,接口網(wǎng)關的設計多種多樣,本文所述其中較為簡單且輕量級的一種。
注:由于筆者經(jīng)驗與知識有限,文章中如有錯誤或偏頗,歡迎探討和指正(作業(yè)部落提供文章按塊批注功能,非常歡迎提批注,筆者會及時修正)。
一、什么是接口網(wǎng)關

1.1 定位
接口網(wǎng)關,顧名思義,是企業(yè) IT 在系統(tǒng)邊界上提供給外部訪問內部接口服務的統(tǒng)一入口。這里的外部可以指客戶端、瀏覽器或者第三方應用等,在這種情況下,接口網(wǎng)關可以有多種定位:
提供后端服務面向 Web App 或者 Mobile App 的 APIGateway
作為開放平臺面向 Partner 的 OpenAPI
...
在筆者的工作中,同樣把面向客戶端的網(wǎng)關稱作 APIGateway,把作為開放平臺提供給第三方服務的網(wǎng)關稱作 OpenApi。本文主要以 OpenApi 作為接口網(wǎng)關為例來講述。
1.2 功能
作為企業(yè) IT 系統(tǒng)的統(tǒng)一入口,接口網(wǎng)關可提供請求路由與組合、協(xié)議轉換、安全認證、服務鑒權、流量控制與日志監(jiān)控等服務。在筆者的工作中,主要在接口網(wǎng)關上實現(xiàn)了請求路由與安全認證的功能,題目中所說的“設計”,主要是指請求路由與安全認證方面,暫不涉及流量控制或日志監(jiān)控等其他方面的設計。

二、為什么需要接口網(wǎng)關
正如上文所言,短信驗證碼平臺接口為企業(yè)應用提供了豐富的功能,而筆者在工作中開發(fā)的接口網(wǎng)關主要提供請求路由與安全認證的功能,那么在回答“為什么需要接口網(wǎng)關”的時候,需要對這兩者多加闡述。
2.1 請求路由
企業(yè)提供內外兩網(wǎng),在沒有接口網(wǎng)關時,提供外部服務的應用需要部署在外網(wǎng)。隨著服務的增多,部署在外網(wǎng)的應用越來越多,在服務的安全壓力與維護成本增大的情況下,需要一個統(tǒng)一的接口網(wǎng)關“隔離”內外服務。企業(yè)提供的服務(無論內部服務還是外部服務)均部署在內網(wǎng),而由部署在外網(wǎng)的網(wǎng)關接受請求,并路由到內網(wǎng)服務。在這種情況下,既有利于對外屏蔽企業(yè)內部服務部署細節(jié),提供統(tǒng)一的服務訪問地址,又便于管理與維護內外部服務接口,便于演進與重構服務。這是接口網(wǎng)關提供請求路由的作用。
2.2 安全認證
在沒有接口網(wǎng)關時,企業(yè)對外服務直接由外部訪問,身份驗證與數(shù)據(jù)加解密等工作都需要每一個對外服務本身去處理,增加了服務本不該有的職責,并且增加了服務開發(fā)的難度與工作量。實際在大多數(shù)情況下,可以將身份驗證與數(shù)據(jù)加解密等安全工作可以從服務抽離,統(tǒng)一由接口網(wǎng)關負責處理。接口網(wǎng)關作為入口,對外驗證調用方的 IP,身份以及接口訪問權限等,并且可以解密數(shù)據(jù)后再將請求路由到服務。這是接口網(wǎng)關提供安全認證的功能。
以上是實際工作中涉及的為什么需要接口網(wǎng)關的其中兩個原因,當然原因遠不止此,有興趣的讀者可以閱讀其他文章,比如 《談API網(wǎng)關的背景、架構以及落地方案》 或者 《微服務:從設計到部署》(英文原文:Microservices: From Design to Deployment)。接下來的章節(jié)我們開始探討如何開發(fā)接口網(wǎng)關。
三、如何開發(fā)接口網(wǎng)關
我們先看看工作中設計的提供請求路由與安全認證功能的接口網(wǎng)關的架構。
不過在介紹接口網(wǎng)關的設計之前,我們先來了解一下關于 Nginx 與 OpenResty 的基礎知識。
3.1 Nginx 與 OpenResty 簡介
3.1.1 Nginx 簡介
Nginx 是世界第二大 Web 服務器,僅次于 Apache,然而由于其極高的性能可處理海量的互聯(lián)網(wǎng)請求,現(xiàn)在已經(jīng)成為業(yè)界高性能 Web 服務器的代名詞。
它的主要特征是高性能、高擴展性、高可靠性、低內存消耗、單機支持 10 萬以上的并發(fā)連接,支持熱部署,以及使用較自由的 BSD 許可協(xié)議。其中,Nginx 可以處理高并發(fā)壓力下的并發(fā)請求的原因如下:
事件驅動模型設計
全異步的網(wǎng)絡 I/O 處理機制
極少的進程切換
內存消耗低,極度“壓榨”服務器硬件資源
除了基于事件驅動的架構使其支持百萬級的 TCP 連接,另外高度模塊化的設計和自由的許可證使其擁有非常多擴展其功能的第三方模塊,也是它的重要特性。所以,后來才會有 OpenResty 的誕生。

我們看一個 Nginx 作簡單配置來提供服務的例子:
worker_processes 1;
events {
worker_connections 1024;
}
http {
upstream backend {
server 127.0.0.1:8080
}
server {
location /back {
proxy_pass http://backend;
}
}
}
上述配置文件中,分別在 event、http、server 以及 location 塊配置項中做了一些簡單的配置,當安裝完并啟動 Nginx后(監(jiān)聽 80 端口),訪問到 /back 路徑下的請求會被轉發(fā)到本地 127.0.0.1:8080 服務上。
3.1.1 OpenResty 簡介
根據(jù)官網(wǎng)定義,OpenResty 是一個通過 Lua 擴展 Nginx 實現(xiàn)的可伸縮的 Web 平臺。其核心是基于 Nginx 一個 C 模塊將 Lua 語言嵌入到 Nginx 服務器中,對外提供一套完整的 Lua Web 的 API,并透明支持非阻塞 I/O,提供協(xié)程 —— “輕量級線程”、定時器等,從而極大地降低了高性能服務端的開發(fā)難度和開發(fā)周期。
OpenResty 將兩個極為優(yōu)秀的組件 Nginx 與 Lua 進行糅合,一方面保留了 Nginx 高性能 web 服務特征,另一方面有提供 Lua 特性在極少損失性能情況下便于業(yè)務功能的開發(fā)。根據(jù)官網(wǎng)介紹,OpenResty 非常便于用來搭建能夠處理超高并發(fā)、擴展性極高的動態(tài) Web 應用、Web 服務和動態(tài)網(wǎng)關。
我們也是因為 OpenResty 的這些特性,特別是它對搭建動態(tài)網(wǎng)關的友好支持,才選擇了基于 OpenResty 來開發(fā)我們的接口網(wǎng)關 —— APIGateway 與 OpenApi。
開發(fā)接口網(wǎng)關使用到的 OpenResty 一個重要知識:OpenResty 對于一個請求的處理流程。Nginx 把一個請求分為不同的階段,從而讓第三方模塊通過掛載行為在不同的階段來定制自己的行為;OpenResty 擁有同樣的特性,不過在不同階段掛載的是 Lua 腳本。下圖是基于《OpenResty 最佳實踐》原圖重繪而來:
從上圖可知,OpenResty 處理請求大致分為四個階段:
初始化階段(Initialization Phase)
重寫與訪問階段(Rewrite / Access Phase)
內容生成階段(Content Phase)
日志記錄階段(Log Phase)
我們看一個 OpenResty 作簡單配置來提供服務的例子:
worker_processes 1;
events {
worker_connections 1024;
}
http {
resolver 127.0.0.1;
lua_package_path '$prefix/lua/?.lua;;';
init_by_lua_block {
# ...
}
init_worker_by_lua_file lua/init_work_by_lua.lua;
server {
listen 80;
location / {
rewrite_by_lua_file lua/rewrite_by_lua.lua;
access_by_lua_file lua/access_by_lua.lua;
proxy_pass http://<url>;
}
}
}
上述配置文件中,分別在 event、http、server 以及 location 塊配置項中做了一些簡單的配置,當安裝完并啟動 Nginx后(監(jiān)聽 80 端口),首先執(zhí)行 init_by_lua_block、init_worker_by_lua_file 進行初始化,接著接受請求,所有的請求都會匹配上 "/" 路徑,進而執(zhí)行 rewrite_by_lua_file、access_by_lua_file 進行重寫與訪問,最后轉發(fā)請求到本地 127.0.0.1 服務上。
在實際的接口網(wǎng)關開發(fā)中,我們主要是使用到了 OpenResty 中初始化階段的 init_by_lua*、init_worker_by_lua*、重寫與訪問階段 的 rewrite_by_lua*、access_by_lua* 以及內容生成階段 content_by_lua* 過程。
3.2 接口網(wǎng)關的架構
這一節(jié)是本文的核心內容,重點講述接口網(wǎng)關的架構設計。如前文所述,本文主要以 OpenApi 為例來講述接口網(wǎng)關的架構設計。先看圖:
下面我們來一步步來分析架構圖的各個部分,首先是兩層的 HAProxy 。
3.2.1 兩層 HAProxy 代理
根據(jù)維基百科定義,HAProxy 是一個使用 C 語言編寫的自由及開放源代碼軟件,其提供高可用性、負載均衡,以及基于 TCP 和 HTTP 的應用程序代理。
如圖所示,隔離的內網(wǎng)與外網(wǎng)上分別提供了 HAProxy 代理, 外層暫且稱為 HAProxy internet ,內層稱為 HAProxy internal。外層暴露于外網(wǎng)中,使用統(tǒng)一地址如 http://openapi.company.com 來接受外部請求(這里指第三方的請求);中間是基于 OpenResty 的 Nginx 網(wǎng)關層,外部請求經(jīng)過網(wǎng)關后通過 HAProxy internal 轉發(fā)到內網(wǎng)的服務上,內網(wǎng)服務遵循 Restful 風格,網(wǎng)關轉發(fā)到內網(wǎng)的地址由接口網(wǎng)關控制。
然而,目前的代理架構受到了當前整體架構的約束,實際上兩層的 HAProxy 代理并不是必需的。
對于外層 HAProxy internet,由于我們使用了與 HAProxy 緊密結合的 Openshift 架構,所以多了一層 HAProxy 的轉發(fā);一般情況下,基于 OpenResty 的 Nginx 網(wǎng)關層可以直接在外網(wǎng)上提供服務。
對于內層的 HAProxy internal,由于我們當前還沒有實現(xiàn)服務治理,所以需要內層的 HAProxy internal 進行一層轉發(fā);當實現(xiàn)了服務治理,可以消除內層 HAProxy 代理,減少轉發(fā)消耗。
在我們當前的系統(tǒng)量級下,這兩層 HAProxy 轉發(fā)消耗非常小可以被接受,所以調整架構的優(yōu)先級還不高,以后再慢慢演進。
3.2.2 接口網(wǎng)關
接下來這一節(jié)是最為重點的接口網(wǎng)關的設計。接口網(wǎng)關主要利用前文所述的 OpenResty 執(zhí)行階段對請求與響應進行流程處理,包括接口地址的重寫,IP 與資源白名單的控制,請求的解密與驗簽,請求的路由以及響應的簽名與加密等。
這里分成主流程,配置服務,安全服務三部分進行講述。
3.2.2.1 主流程設計
主流程是網(wǎng)關的核心,是請求處理的控制中心;它是通過 OpenResty 的 Lua 腳本處理流程來實現(xiàn)對請求的處理。
A. 主流程
在 OpenResty 服務啟動之后,首先通過 init_by_lua_block 階段初始化常量(包括調用配置服務以及安全服務所需的主機地址、端口、URL 地址等)、引入依賴(包括常用的 http 以及 cjson 依賴等)等作為全局使用;
接著通過 init_worker_by_lua_file 階段設置定時任務調用內網(wǎng)配置服務來緩存配置,為處理第三方的請求做準備,其中加載的配置可供 URL 重寫(即接口映射)、IP 以及資源(URI)白名單限制、請求的解密驗簽以及響應的簽名加密使用,詳情查看配置服務一節(jié)。
當?shù)谌秸埱笸ㄟ^ HAProxy Internet 進入到網(wǎng)關后,根據(jù)配置通過 rewrite_by_lua_file 階段做 URL 重寫(即接口映射)。
服務接口 URL 發(fā)生變更,為了兼容舊的第三方調用,需要重寫第三方請求 URL 到新服務接口上
Restful 接口的 Path Variable 在 Nginx 環(huán)境與在 Tomcat 環(huán)境上正則匹配的差異
需要重寫的原因可能有:
URL 重寫后,通過 access_by_lua_file 進入訪問控制階段,此時根據(jù)授權的第三方 IP 白名單列表,授權予第三方的開放接口列表,校驗請求的 IP 以及 URL。
IP 與 URI 校驗通過后,同樣在 access_by_lua_file 階段根據(jù)配置調用內網(wǎng)的安全服務進行請求的解密與驗簽,獲取明文。
在 content_by_lua_file 階段通過 ngx.location.capture 將原請求頭部信息以及參數(shù)等信息封裝到子請求中,借助子請求轉發(fā)原請求到開發(fā)接口服務中。
注意:根據(jù)官方文檔說明,ngx.location.capture 發(fā)送子請求會緩存響應在內存中,直到整個請求處理結束。那么,當有響應報文特別長或者請求并發(fā)非常高時,需要使用 cosocket 來替代 ngx.location.capture,避免因內存不足造成網(wǎng)關服務失效。
同樣在 content_by_lua_file 階段根據(jù)配置調用安全服務進行響應的簽名與加密,獲取簽名與密文返回給第三方。
B. 文件結構
項目的大致結構如下,主要分為 Lua 代碼目錄和環(huán)境配置目錄。
--openapi
--lua
--access_by_lua.lua
--cache_management.lua
--content_by_lua.lua
--init_work_by_lua.lua
--rewrite_by_lua.lua
--security.lua
--prod
--Dockerfile
--nginx.conf
--sit
--Dockerfile
--nginx.conf
--README.md
C. 主流程在 conf 中的配置
# Nginx worker 進程個數(shù),直接影響性能。
# 如果確認不會出現(xiàn)阻塞式調用,那么有多少 CPU 內核設置多少個進程
# 如果有可能出現(xiàn)阻塞式調用,需要配置多一些進程
worker_processes 1;
events {
worker_connections 1024;
}
http {
# 內網(wǎng)地址
resolver xxx.x.x.xxx yyy.y.y.yyy;
# 日志格式配置
log_format graylog2_format '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'<msec=$msec|connection=$connection|connection_requests=$connection_requests|millis=$request_time>';
# 日志路徑配置
access_log syslog:server=<host>:<port> graylog2_format;
error_log syslog:server=<host>:<port> warn;
# 配置 Lua 包地址
lua_package_path '$prefix/lua/?.lua;;';
init_by_lua_block {
# 引入依賴(可能會污染全局環(huán)境,待研究)
http = require "resty.http"
cjson = require "cjson"
cache_management = require "cache_management"
...
}
# 設置定時任務緩存配置,及上面的 cache_management 模塊
init_worker_by_lua_file lua/init_work_by_lua.lua;
# Nginx Web 服務配置
server {
listen 80;
# ngx.location.capture 子請求代理,轉發(fā)原請求到接口服務
location = /ngx_proxy/ {
internal;
proxy_set_header Accept-Encoding '';
proxy_pass http://$context$http_host_suffix$proxy_uri;
}
# 匹配所有請求,進行 URL 重寫、訪問控制、轉發(fā)請求以及響應處理(各階段的處理在此配置)。
location / {
set $context '';
...
rewrite_by_lua_file lua/rewrite_by_lua.lua;
access_by_lua_file lua/access_by_lua.lua;
content_by_lua_file lua/content_by_lua.lua;
}
}
}
D. URL 規(guī)范
內網(wǎng)服務遵循的 URL 格式為 http://<host>:<port>/<context>/path/to/your/api,應用上下文根緊跟在 <host>:<port>之后,以便統(tǒng)一獲取來找到配置。比如:http://172.0.8.177:8080/user/users/{uid}/info,其中 user 為應用上下文根,緊跟在 172.0.8.177:8080 之后。
E. 樣例
內網(wǎng)用戶信息服務由原來的 API:/user/users/{uid}/info 提供,后來遷移至 API:/user/users/{uid}/user-info,當?shù)谌?CampA (IP 為 172.0.1.172) 發(fā)起 GET 請求時,請求 URL 為 http://openapi.company.com/user/users/27/info?thirdparty=CampA&cp=fj375x...sign=abxuos8nb...。
初始化常量和依賴等
通過 CampA 與 user Context 獲取第三方配置
HAProxy Internet 接收請求發(fā)到 OpenApi 接口網(wǎng)關,OpenApi 把 /user/users/27/info URI 重寫為 /user/users/27/user-info/
校驗第三方請求 IP,在 IP 白名單中,校驗通過;校驗 URI /user/users/27/user-info 在授權的 URI 中,校驗通過
調用安全服務對請求進行解密與驗簽,解密成功,驗簽通過,獲取明文
將擁有明文的請求轉發(fā)到開放接口服務
獲取響應,調用安全服務對響應報文進行簽名與加密,返回給第三方 CampA。
3.2.2.2 配置服務設計
A. 數(shù)據(jù)庫表設計
openapi_thirdparty_config
id third_party need_check_ip ips req_need_verify_sign resp_need_sign
1 CompA 1 172.0.25.187, 172.0.25.188 1 1
openapi_api_config
id third_party method url req_need_decrypt resp_need_encrypt
1 CompA GET /user/users/[^/]+/userinfo 1 1
openapi_api_mapping
id from_api to_api
1 GET /old/1/user/users/(.+)/userinfo GET /users/$1/userinfo
B. 配置服務接口響應
{
# 接口映射配置
"apiMapping":{
"$context":{
"$fromApi":"$toApi"
}
},
# 接口白名單配置、加解密配置
"apiConfig":{
"$channel $context":{
"$httpMethod $uri":{
"reqNeedDecrypt":false,
"respNeedEncrypt":false
}
}
},
# IP 白名單配置,驗簽名配置
"channelConfig":{
"$channel":{
ips:{
"$ip":1
},
"reqNeedVerifySign":false,
"respNeedSign":false,
"needCheckIp":false
}
}
}
3.2.2.3 安全服務設計
為了保證請求或響應的完整性、以及請求或響應來源的合法性,雙方傳輸需要進行簽名;另外,由于可能開放接口的請求或響應會包含敏感信息,需要進行加密傳輸。這里的安全服務就是指請求的解密與驗簽和響應的簽名與加密服務。
A. 算法約定
對稱加密算法:3DES(DESede/ECB/PKCS5Padding)
非對稱加密算法:RSA(RSA/ECB/PKCS1Padding)
簽名算法:SHA1WithRSA
B. 公鑰約定
雙方預先交換 RSA 公鑰
雙方公鑰編碼方式:UTF-8 編碼的 Base64String
雙方進行加解密與驗簽名可使用同一把 RSA 公私鑰或者分別使用各自的公私鑰,雙方約定即可
C. 第三方請求流程示意
其中,添加統(tǒng)一參數(shù)為必選步驟,請求簽名、請求加密、響應解密、以及響應驗簽都是可選步驟。
無論是 GET、POST 或者其他方式的請求,第三方在訪問平臺開放接口前,都需要添加統(tǒng)一參數(shù)到 request parameter 中
統(tǒng)一參數(shù)包括第三方應用名、請求時間戳、隨機不重復字符串 nonce 等
驗簽名屬于應用維度 —— 針對應用做驗簽名(比如:按照約定需要對第三方應用 A 進行驗簽,則應用 A 訪問平臺任何接口都需要簽名)
加解密屬于接口維度 —— 針對接口做加解密(比如:同一個第三方訪問 A 接口需要加密,而訪問 B 接口可以不需加密)
D. 加解密示意(以第三方請求為例)
E. 驗簽名示意(以第三方請求為例)
3.2.3 架構總結
由于 Nginx 與 Lua 本身杰出的性能,在當前的系統(tǒng)量級與整體 IT 架構下,我們使用這樣的接口網(wǎng)關架構已經(jīng)可以支撐較大的并發(fā)請求。在最后的這一節(jié),我們不妨回顧一下前文講述的接口網(wǎng)關架構,看看目前性能上仍存在著的兩個主要待改進的地方。
兩層 HAProxy 代理:在使用更優(yōu)產(chǎn)品替代 Openshift 架構的情況下,直接部署接口網(wǎng)關到公網(wǎng),可消除外層 HAProxy 代理;在實現(xiàn)服務治理的情況下,由接口網(wǎng)關直接轉發(fā)請求到服務,可消除內層 HAProxy 代理。
安全服務性能:加解密驗簽名等安全服務是以內部服務的方式提供給接口網(wǎng)關,而且使用了性能不太好的 ngx.location.capture 轉發(fā)原請求,在系統(tǒng)量級增大后會遇到性能瓶頸,可通過使用高性能的 Lua 腳本在接口網(wǎng)關層提供安全服務,從而提升安全服務性能。
除了以上主要的兩點,隨著系統(tǒng)量級的提升與整體 IT 架構的演進,接口網(wǎng)關的架構也會隨之調整和演進,在各個方面都盡可能地優(yōu)化性能,以適應更大系統(tǒng)量級的需求。