王正華



關(guān)鍵詞:數(shù)據(jù)查詢;服務(wù)接口;元數(shù)據(jù);數(shù)據(jù)庫;系統(tǒng)安全
0 引言
人保財險江蘇省分公司2022年保費收入近500 億元,是整個財產(chǎn)保險行業(yè)最大的一家省級分支機(jī)構(gòu)。為支持公司業(yè)務(wù)發(fā)展,江蘇省分公司開發(fā)了眾多適合公司管理需求的應(yīng)用系統(tǒng)。近年來,為順應(yīng)微服務(wù)架構(gòu)和中臺戰(zhàn)略的潮流,公司積極進(jìn)行應(yīng)用的改造,基于微服務(wù)將原來的單體應(yīng)用架構(gòu)升級為分布式架構(gòu),同時為提高安全性,將跨應(yīng)用的數(shù)據(jù)庫直接訪問改造成服務(wù)接口調(diào)用。在進(jìn)行應(yīng)用間調(diào)用關(guān)系的梳理后,發(fā)現(xiàn)大部分調(diào)用為數(shù)據(jù)查詢調(diào)用,如果每個查詢服務(wù)都進(jìn)行獨立開發(fā),將耗費大量的資源,因此決定開發(fā)一套基于元數(shù)據(jù)的通用查詢接口系統(tǒng),通過界面進(jìn)行查詢配置,快速生成查詢服務(wù)接口,加快應(yīng)用改造的進(jìn)度,也為后續(xù)應(yīng)用開發(fā)打下堅實的基礎(chǔ)。
1 系統(tǒng)概述
系統(tǒng)采用Java 作為開發(fā)語言,基于傳統(tǒng)的三層B/S架構(gòu),Spring Boot構(gòu)建后端應(yīng)用,前端通過HTML+CSS+JavaScript(jQuery) 提供WEB配置和管理界面。
系統(tǒng)支持單機(jī)部署或集群部署,單機(jī)部署時使用Ehcache緩存,集群部署時使用Redis緩存,通過F5或Nginx進(jìn)行負(fù)載均衡。
系統(tǒng)主要由元數(shù)據(jù)配置、數(shù)據(jù)源動態(tài)管理、數(shù)據(jù)查詢接口、接口安全控制、日志監(jiān)控和配置元數(shù)據(jù)庫及數(shù)據(jù)緩存等部分構(gòu)成,其中數(shù)據(jù)源動態(tài)管理、數(shù)據(jù)查詢接口和接口安全控制是整個系統(tǒng)的核心。
總體架構(gòu)如圖1所示。
2 系統(tǒng)設(shè)計
2.1 數(shù)據(jù)源動態(tài)管理
系統(tǒng)提供配置界面配置數(shù)據(jù)源名稱、數(shù)據(jù)庫連接的驅(qū)動、連接串、用戶名和密碼以及連接池的大小等參數(shù)。系統(tǒng)采用Druid連接池管理數(shù)據(jù)庫連接,開發(fā)連接池管理器,在內(nèi)存中建立數(shù)據(jù)源名稱與連接池之間的映射關(guān)系,對連接池進(jìn)行命名管理。連接池按需創(chuàng)建,系統(tǒng)僅創(chuàng)建實際查詢需要的連接池。
注冊連接池時,連接池管理器創(chuàng)建DruidData?Source連接池對象,并添加到名稱與連接池映射表。
刪除連接池時,連接池管理器從映射表中摘除名稱對應(yīng)的連接池,并將連接池添加到待刪除連接池隊列,由連接池關(guān)閉線程在連接池?zé)o連接時執(zhí)行關(guān)閉操作并刪除連接池。
連接池的連接數(shù)等參數(shù)調(diào)整時,連接池管理器從映射表取得對應(yīng)的連接池,修改對應(yīng)的連接池參數(shù)。
系統(tǒng)查詢數(shù)據(jù)時,連接池管理器從映射表取得對應(yīng)的連接池,并通過連接池取得數(shù)據(jù)庫連接,如數(shù)據(jù)庫連接池尚未創(chuàng)建,則創(chuàng)建連接池。
集群部署時,系統(tǒng)啟用連接池發(fā)現(xiàn)線程定期從元數(shù)據(jù)表中查詢連接池的配置,如發(fā)現(xiàn)配置變更則參照上述操作調(diào)整連接池。
2.2 數(shù)據(jù)查詢接口
數(shù)據(jù)查詢接口是一個遵循REST 架構(gòu)風(fēng)格的API[1],支持根據(jù)配置的元數(shù)據(jù)從關(guān)系數(shù)據(jù)庫中查詢滿足條件的數(shù)據(jù),并以適當(dāng)?shù)母袷椒祷卣{(diào)用者。
2.2.1 元數(shù)據(jù)配置
系統(tǒng)提供配置界面配置數(shù)據(jù)查詢接口元數(shù)據(jù),數(shù)據(jù)查詢接口配置項目包括:
1) 查詢的數(shù)據(jù)表,如果查詢多個數(shù)據(jù)表,定義表之間的連接類型(內(nèi)連接、左右外連接等)以及連接條件,系統(tǒng)根據(jù)數(shù)據(jù)庫元數(shù)據(jù)可自動生成連接條件。
2) 查詢的項目,支持原始的數(shù)據(jù)表字段、聚合函數(shù),以及基于上述查詢項目的表達(dá)式運(yùn)算。
3) 查詢的條件,分為固有條件和客戶端請求條件兩類??蛻舳苏埱髼l件定義客戶端請求參數(shù)對應(yīng)的數(shù)據(jù)表字段或聚合函數(shù),以及實施的控制類型。控制類型包括是否允許模糊匹配、是否為必選項(單個項目必選或一組項目中至少一個必選)。接口查詢元數(shù)據(jù)配置如圖2所示。
2.2.2 關(guān)鍵技術(shù)
作為通用的數(shù)據(jù)查詢接口,支持常見的數(shù)據(jù)庫類型是系統(tǒng)的基本要求。這方面,系統(tǒng)重點解決以下三個問題:
1) 獲取數(shù)據(jù)庫表結(jié)構(gòu)以及數(shù)據(jù)表之間的關(guān)系,用于數(shù)據(jù)查詢接口元數(shù)據(jù)的配置、查詢條件的解析以及查詢結(jié)果輸出格式的選擇。然而不同的數(shù)據(jù)庫系統(tǒng),其數(shù)據(jù)字典存儲的位置也不盡相同,比如Informix使用systables和syscolumns存儲數(shù)據(jù)表和字段的定義,而PostgreSQL使用pg_class和pg_attribute存儲數(shù)據(jù)表和字段的定義。
2) 在涉及大量數(shù)據(jù)查詢時,需要使用合適的參數(shù)進(jìn)行分頁處理,但是各種數(shù)據(jù)庫在分頁處理上也千差萬別,如Informix數(shù)據(jù)庫(v11以上版本)使用“SELECTSKIP m FRIST n …”查詢第m條起共n條數(shù)據(jù),而Post?greSQL 數(shù)據(jù)庫使用“SELECT … OFFSET m LIMIT n”實現(xiàn)Informix類似功能。
3) 不同數(shù)據(jù)庫系統(tǒng)對GROUP BY和ORDER BY 格式要求不同,如Informix可以使用數(shù)字序號代替查詢字段,而Oracle要求使用查詢字段名稱;Informix排序字段可以不出現(xiàn)在查詢結(jié)果中,而PostgreSQL要求排序字段必須是查詢字段。
本系統(tǒng)從兩個層面解決上述問題:
1) 通過JDBC的DatabaseMetaData獲取數(shù)據(jù)庫共性元數(shù)據(jù)[2]。
JDBC中,描述數(shù)據(jù)庫表等數(shù)據(jù)字典的數(shù)據(jù)稱為元數(shù)據(jù)。系統(tǒng)通過數(shù)據(jù)庫連接獲得DatabaseMetaData 對象,并使用該對象查詢以下內(nèi)容:①數(shù)據(jù)庫中包含的數(shù)據(jù)表、同義詞或視圖(以下如無特殊說明將數(shù)據(jù)表、同義詞及視圖統(tǒng)稱為數(shù)據(jù)表)的Catalog,Schema及數(shù)據(jù)表名。
②數(shù)據(jù)表包含的字段以及字段的類型、長度和精度。
③數(shù)據(jù)表(不包括同義詞和視圖)主鍵字段、表之間的主外鍵關(guān)聯(lián)關(guān)系。
2) 通過方言接口實現(xiàn)數(shù)據(jù)庫的差異化處理。
系統(tǒng)定義數(shù)據(jù)庫方言接口,不同的數(shù)據(jù)庫實現(xiàn)差異化的實現(xiàn)類。接口定義了以下方法:
①getSupportFeatures()
取得數(shù)據(jù)庫支持的功能特性,主要用于檢查數(shù)據(jù)庫是否支持指定查詢起始記錄序號、GROUP BY 和ORDER BY的表示方式和限制條件等。
②splitTableName(String name)
將數(shù)據(jù)表名拆分為Catalog、Schema和Table名稱三個部分,以便以合適的參數(shù)調(diào)用DatabaseMetaData 中的方法獲取數(shù)據(jù)表元數(shù)據(jù)。
③addLimit(String sql, int start, int count)
在SQL語句中增加查詢范圍限制條件。
數(shù)據(jù)庫方言工廠通過數(shù)據(jù)庫連接的Databas?eMetaData獲得數(shù)據(jù)庫名稱及版本,創(chuàng)建適配的數(shù)據(jù)庫方言對象。
系統(tǒng)實現(xiàn)了Oracle、SQL Server、DB2、Informix、MySQL、PostgreSQL、Greenplum 和SQLite 等數(shù)據(jù)庫的方言,同時提供了支持其他數(shù)據(jù)庫的擴(kuò)展能力。
2.3 接口安全控制
接口安全控制模塊對查詢請求進(jìn)行客戶端身份認(rèn)證、防篡改、超時訪問、地址白名單、流量管控和查詢項目鑒權(quán)等安全檢查,并保存訪問日志記錄[3]。
系統(tǒng)使用過濾器實現(xiàn)接口的安全控制,過濾器前置于數(shù)據(jù)查詢接口,只有過濾器檢查通過的請求才能到達(dá)實際的數(shù)據(jù)查詢接口。
2.3.1 元數(shù)據(jù)配置
元數(shù)據(jù)配置功能提供調(diào)用者接入代碼、簽名密鑰、白名單地址、允許訪問的接口代碼、訪問流量限制等配置項目,實現(xiàn)上述安全控制功能。
系統(tǒng)定義安全檢查接口,簽名驗證、地址白名單檢查、請求超時檢查以及流量管控等功能實現(xiàn)該接口,安全檢查時,系統(tǒng)根據(jù)安全規(guī)則中配置的檢查類型使用工廠方法創(chuàng)建安全檢查對象對請求進(jìn)行檢查,只有通過全部安全檢查的請求才予以放行。
2.3.2 關(guān)鍵技術(shù)
1) 接口參數(shù)的簽名
接口參數(shù)簽名是接口安全的重要組成部分,通過數(shù)據(jù)簽名,可以進(jìn)行用戶身份識別以及參數(shù)的防篡改,配合時間戳完成超時訪問的控制,避免簽名密鑰的暴力破解。
系統(tǒng)為每個接口的調(diào)用方分配一個接入代碼和簽名密鑰,客戶端使用簽名密鑰對請求參數(shù)進(jìn)行簽名,系統(tǒng)使用同樣的算法對簽名進(jìn)行校驗。
請求參數(shù)的簽名算法如下:
①按參數(shù)名稱的字母(區(qū)分大小寫)順序排列請求參數(shù),如兩個參數(shù)name和address:
②連接按參數(shù)名稱排列的參數(shù)值:“江蘇省南京市長江路69號”+“張三”=“江蘇省南京市長江路69號張三”
③添加接入代碼(以test為例),當(dāng)前時間戳和簽名密鑰(以Doak!937為例):“test”+“江蘇省南京市長江路69 號張三”+Sys?tem.currentTimeMillis()+“Doak!937”=“test江蘇省南京市長江路69號張三1688434016264Doak!937”
④計算SHA256的哈希值(使用UTF-8編碼),得到簽名值:DigestUtils. sha256Hex(src. getBytes(StandardChar?sets.UTF_8))=“d0058403e6a1cf3f16e7e0187d4ec92c0da17d31ce3a483ab946c8dc9fa87cef”調(diào)用者在實際請求時,除原參數(shù)外添加接入代碼(appCode) 、時間戳(time) 和簽名值(sign) 構(gòu)建完整的請求參數(shù)。
2) 流量管控
流量管控依賴歷史調(diào)用統(tǒng)計數(shù)據(jù),而接口調(diào)用為高頻操作,如果實時地存取數(shù)據(jù)庫中的統(tǒng)計數(shù)據(jù),將執(zhí)行大量的SQL請求,嚴(yán)重影響系統(tǒng)性能。
系統(tǒng)采用數(shù)據(jù)緩存保存調(diào)用統(tǒng)計數(shù)據(jù),查詢時直接使用緩存數(shù)據(jù),操作成功或失敗后更新緩存數(shù)據(jù),通過獨立的數(shù)據(jù)持久化線程,每分鐘執(zhí)行一次將最近一分鐘的日志數(shù)據(jù)批量保存到數(shù)據(jù)表,達(dá)到削峰平谷的效果。當(dāng)系統(tǒng)異常重啟時,首先將數(shù)據(jù)庫中歷史數(shù)據(jù)加載到緩存,以保證流量管控的正確運(yùn)行。
為控制流量日志數(shù)據(jù)表的大小,系統(tǒng)每天將超過時效的日志數(shù)據(jù)轉(zhuǎn)儲到歷史數(shù)據(jù)表,供日志監(jiān)控功能查詢。
系統(tǒng)支持以分鐘、小時及天為單位進(jìn)行流量控制。系統(tǒng)為每對調(diào)用者/接口分別按需建立一組包含12個元素的循環(huán)隊列,每個元素對應(yīng)一個時間片,圖4 和圖5分別為分鐘(單位秒)、小時(單位分)和天(單位小時)時間片劃分。
系統(tǒng)記錄循環(huán)隊列列頭位置以及對應(yīng)的時間點,時間點計算公式如下:
分鐘時間片時間點=System.currentTimeMillis()/1000/5小 時時間片時間點=System.currentTimeMillis()/
1000/60/5 天時間片時間點=System. currentTimeMillis()/1000/3600/2
當(dāng)客戶端請求到達(dá)時,系統(tǒng)計算當(dāng)前時間對應(yīng)的時間點,與隊列頭時間點進(jìn)行比較,如果值不相同,則移動隊列頭到適當(dāng)位置,同時清空過期的元素,最后更新隊列頭的元素值。
單機(jī)部署環(huán)境下,系統(tǒng)使用JVM內(nèi)存保存時間片隊列。
集群環(huán)境下,系統(tǒng)使用Redis的哈希表模擬循環(huán)隊列[4],哈希表中命名為item0,...,item11的元素對應(yīng)隊列的第0到第11個元素,head表示隊列頭元素名,time 表示隊列頭對應(yīng)的時間點,lock表示隊列鎖。當(dāng)請求的時間點與隊列頭時間點不一致時,使用HSETNX指令鎖定隊列,更新隊列數(shù)據(jù)后釋放隊列鎖,為避免重復(fù)更新,鎖定后進(jìn)行二次檢查。最后使用HINCBY指令增加統(tǒng)計記數(shù)。
3) 客戶端地址的識別
集群部署使用負(fù)載均衡器場景下,使用HttpServ?letRequest 的getRemoteAddr 方法已經(jīng)無法獲得客戶端原始IP地址,導(dǎo)致白名單檢查錯誤。這就需要在系統(tǒng)部署時由負(fù)載均衡器在請求頭中添加XForwarded-For記錄通過負(fù)載均衡的地址列表[5],系統(tǒng)在解析請求頭值后,獲得客戶端原始地址。
實際應(yīng)用中,客戶端(尤其是互聯(lián)網(wǎng)客戶端)可能會通過構(gòu)造X-Forwarded-For請求頭,將非法的地址偽裝成合法的請求地址,為此系統(tǒng)設(shè)置可信負(fù)載均衡器地址參數(shù),只認(rèn)可X-Forwarded-For請求地址中可信負(fù)載均衡器地址的上一級地址。
3 結(jié)束語
實踐表明,基于元數(shù)據(jù)的通用數(shù)據(jù)查詢接口系統(tǒng)顯著地提升了查詢類接口對接效率,單個查詢接口實現(xiàn)時長由以前的平均2個工作日縮短到半個小時,而且杜絕了直連數(shù)據(jù)庫查詢的現(xiàn)象,增強(qiáng)了系統(tǒng)的安全性。系統(tǒng)上線后,配置各類查詢240個,日均訪問量9 萬余次,取得了良好的效果。