1.需求背景
隨著各業(yè)務(wù)系統(tǒng)逐步完成服務(wù)化改造并接入注冊中心,隨即而來的服務(wù)注冊與調(diào)用的安全問題有待解決
首先,原生的Eureka作為服務(wù)注冊中心對服務(wù)注冊沒有權(quán)限控制,服務(wù)可以任意注冊,而且通過接口可以獲取所有已注冊的服務(wù)信息,存在很大的安全隱患。
其次,外部請求以及跨Space的服務(wù)請求存在惡意攻擊的風(fēng)險,需要防范和識別這兩類請求,保障服務(wù)安全調(diào)用
2.設(shè)計目標(biāo)
- 對各網(wǎng)絡(luò)區(qū)域的業(yè)務(wù)系統(tǒng)進(jìn)行邏輯劃分,每個業(yè)務(wù)系統(tǒng)為一個Space
- 不同Space之間進(jìn)行網(wǎng)絡(luò)策略隔離,需通過認(rèn)證服務(wù)器(Authorization server)拿到Token以及API網(wǎng)關(guān)授權(quán)后,才能訪問其他Space資源;只有在特殊情況下才允許將Space間網(wǎng)絡(luò)打通并直接調(diào)用
- 每個Space需有獨立的網(wǎng)關(guān)服務(wù),為確保高可用,網(wǎng)關(guān)以集群方式提供,并按照部署環(huán)境的不同(容器云、虛擬機)確定是否部署nginx
- 每一個Space的所有服務(wù)共享注冊中心,服務(wù)通過認(rèn)證后才能訪問注冊中心
- 服務(wù)調(diào)用安全方案落地需要各業(yè)務(wù)系統(tǒng)配合,相關(guān)接入邏輯擬與已發(fā)布的eureka starter進(jìn)行整合,降低推廣和接入成本
3.技術(shù)選型
Spring Cloud Zuul + Spring Security + JWT + activiti工作流 + mysql
4.解決方案
4.1 系統(tǒng)物理部署圖(虛擬機環(huán)境部署方案,非容器)

如上圖所示,各業(yè)務(wù)系統(tǒng)前端或授權(quán)管理系統(tǒng)前端靜態(tài)頁面都部署于web區(qū);API網(wǎng)關(guān)使用Spring Cloud Zuul實現(xiàn),為確保Zuul高可用,API網(wǎng)關(guān)采用集群形式部署,并由nginx做負(fù)載均衡,需要注冊在各個space的注冊中心。
4.1.1 服務(wù)調(diào)用類型
根據(jù)設(shè)計目標(biāo),服務(wù)間調(diào)用主要分為以下三種類型:
- 跨Space之間的調(diào)用
- 同一Space內(nèi)的服務(wù)調(diào)用
4.1.2 技術(shù)方案
對于同一個Space內(nèi)的兩個服務(wù)之間的調(diào)用,不需要經(jīng)過nginx和網(wǎng)關(guān),在注冊中心注冊后通過ribbon直接調(diào)用。
對于不在同一個Space的兩個服務(wù),服務(wù)消費者得請求都要經(jīng)過nginx分發(fā)到目標(biāo)網(wǎng)關(guān),授權(quán)成功后再向服務(wù)提供者發(fā)起請求。
4.2 服務(wù)認(rèn)證、授權(quán)全流程

完整的服務(wù)調(diào)用認(rèn)證授權(quán)流程如上圖所示,對流程分解如下:
-
授權(quán)認(rèn)證準(zhǔn)備
當(dāng)服務(wù)需要對外提供接口時,需要在認(rèn)證授權(quán)管理系統(tǒng)中登記,說明要對外開放的接口信息,當(dāng)有服務(wù)需要調(diào)用該接口時,需要發(fā)起申請審批流程 -
授權(quán)認(rèn)證流程
同一個Space內(nèi)的服務(wù)是互信的,所以Space N內(nèi)的Service C可以直接請求Service A。
但是如果Service A需要跨Space請求Service B,則需要完成認(rèn)證授權(quán)流程。 -
服務(wù)消費方
服務(wù)消費方Service A接入安全組件(Jar包形式),安全組件使用RestTemplate攔截器是調(diào)用請求帶上http header(包括clientName,clientSecret)去請求 OAuth2 Server 獲取 JWT,(注意:安全組件會定時采集JWT并存入內(nèi)存中,以保證不用每次都通過網(wǎng)絡(luò)去請求JWT)然后生成帶有JWT頭信息的RestTemplate請求。 -
認(rèn)證服務(wù)器
OAuth2 Server 從接受請求到返回JWT,具體步驟如下:
OAuth2 Server 去數(shù)據(jù)庫查詢 client 表,驗證 clientName、clientSecret 是否正確,如正確跳轉(zhuǎn)至下一步。
OAuth2 Server 根據(jù) Service A 傳過來的 clientName 查 client 表獲取對應(yīng)的 Space 和 Space 的私鑰
OAuth2 Server 將 clientName 與 clientId封裝至 JWT ,并使用上一步獲取的私鑰對 JWT 進(jìn)行簽名 -
服務(wù)消費方
服務(wù)消費方 Service A 帶上獲取到的 JWT(在 http header 中),通過 Space M 的 網(wǎng)關(guān)將請求分發(fā),去訪問 Service B ,但是在網(wǎng)關(guān)分發(fā)請求前,必須通過授權(quán) -
API網(wǎng)關(guān)
服務(wù)消費方 Service A 的請求分發(fā)到 API網(wǎng)關(guān) 時,網(wǎng)關(guān)的授權(quán)流程如下:

如上圖所示,當(dāng) JWT 非空時,利用 Base64 反編碼解析出 clientId,并根據(jù) clientId 查找本地緩存中對應(yīng)的公鑰,若本地緩存不存在則從 OAuth2 Server 請求公鑰并寫入本地緩存;利用緩存的公鑰和 JWT 進(jìn)行驗簽,通過則跳轉(zhuǎn)至下一步;根據(jù)clientId查找本地緩存中對應(yīng)的URI,與當(dāng)前請求的URI比對,判斷服務(wù)消費方是否有權(quán)訪問此URI;如果此用戶具有訪問此URI的權(quán)限,網(wǎng)關(guān)放行,如無權(quán)限則直接拒絕,返回401代碼給服務(wù)消費方。
- API網(wǎng)關(guān)通過 Ribbon,代理請求到服務(wù)提供方,即 Service B
- 服務(wù)提供方處理完請求后,通過網(wǎng)關(guān)將調(diào)用結(jié)果返回到服務(wù)消費方
5. 總結(jié)
如圖,位于 Space N 的 Service A 需要遠(yuǎn)程調(diào)用位于 Space M 的 Service B
具體的認(rèn)證授權(quán)流程為:
5.1 安全接入組件(獲取JWT的過程不會侵入業(yè)務(wù)代碼,通過restTemplate攔截器實現(xiàn))
1. Service A 帶上http header(包括clientId,clientSecret)去訪問OAuth Server,OAuth Server從接收請求到返回 JWT 需要如下步驟:
?1.1 OAuth Server 去數(shù)據(jù)庫查詢 client 表,驗證clientName、clientSecret是否正確,如正確跳轉(zhuǎn)至步驟1.2
?1.2 OAuth Server 查詢表 client,獲取到 client 表的主鍵 clientId
?1.3 OAuth Server 根據(jù) Service A 傳過來的 clientName 查 client 表獲取該client的私鑰
?1.4 OAuth Server 將 client 表的 id 字段封裝至 JWT,并使用步驟1.3中獲取的私鑰對 JWT 進(jìn)行簽名
Service A 獲取 JWT 后存儲到內(nèi)存,只有在 JWT 快要超時時,才會再次向 OAuth Server 發(fā)送獲取 JWT 請求,在超時時間內(nèi),Service A 直接使用內(nèi)存中的 JWT 進(jìn)行遠(yuǎn)程調(diào)用
2. Service A 帶上 JWT 去訪問 Space M 的 API網(wǎng)關(guān),首先會通過 Space M 的 nginx 對請求分發(fā)
3. Service A 的調(diào)用分發(fā)到 API 網(wǎng)關(guān)時,網(wǎng)關(guān)的具體執(zhí)行步驟如下:
5.2 網(wǎng)關(guān)
?3.1 網(wǎng)關(guān)初次啟動時,會調(diào)用批量查詢公鑰、批量查詢權(quán)限列表接口,并緩存到本地Map(pubKeyCacheData、uriCacheData)
?3.2 當(dāng)接收到一個請求時,取出 header 里的 access_token 首先對其進(jìn)行 Base64 反編碼,獲取到 clientId
?3.3 拿到 clientId 后,用 3.1 獲取的 clientId 的 對應(yīng)公鑰對 JWT 驗簽,驗簽通過后與內(nèi)存中的 uriCacheData 進(jìn)行對比,判斷調(diào)用者是否有權(quán)訪問此 URI
注:為了降低數(shù)據(jù)庫的壓力,API網(wǎng)關(guān)采用失敗再查詢方式,通過 OAuth Server 提供的接口更新本地 client-List<uri> 對應(yīng)關(guān)系,從而不用每來一次請求就查數(shù)據(jù)庫
?3.4 如用戶具有訪問此 URI 的權(quán)限,網(wǎng)關(guān)放行,如再次查詢后無權(quán)限則直接拒絕,返回 401 代碼給 Servce A
4. API網(wǎng)關(guān)通過從 Eureka 獲取的路由信息,轉(zhuǎn)發(fā)請求到 Service B
5. 服務(wù)B處理完成后,通過網(wǎng)關(guān)將調(diào)用結(jié)果返回到 Service A
6 OAuth2
API網(wǎng)關(guān)認(rèn)證授權(quán)是采用的 OAuth2 的客戶端模式
OAuth是一個關(guān)于授權(quán)(authorization)的開放網(wǎng)絡(luò)標(biāo)準(zhǔn),目前的版本是2.0版。只要授權(quán)方和被授權(quán)方遵守這個協(xié)議去寫代碼提供服務(wù),那雙方就是實現(xiàn)了OAuth模式。
名詞定義:
(1) Third-party application:第三方應(yīng)用程序,本文中又稱"客戶端"(client),即上一節(jié)例子中的"云沖印"。
(2)HTTP service:HTTP服務(wù)提供商,本文中簡稱"服務(wù)提供商",即上一節(jié)例子中的Google。
(3)Resource Owner:資源所有者,本文中又稱"用戶"(user)。
(4)User Agent:用戶代理,本文中就是指瀏覽器。
(5)Authorization server:認(rèn)證服務(wù)器,即服務(wù)提供商專門用來處理認(rèn)證的服務(wù)器。
(6)Resource server:資源服務(wù)器,即服務(wù)提供商存放用戶生成的資源的服務(wù)器。它與認(rèn)證服務(wù)器,可以是同一臺服務(wù)器,也可以是不同的服務(wù)器。
OAuth2一共有四種模式:授權(quán)碼模式、簡化模式、密碼模式、客戶端模式
這里只介紹用的比較多的授權(quán)碼模式和客戶端模式
6.1 授權(quán)碼模式

它的步驟如下:
(A)用戶訪問客戶端,后者將前者導(dǎo)向認(rèn)證服務(wù)器。
(B)用戶選擇是否給予客戶端授權(quán)。
(C)假設(shè)用戶給予授權(quán),認(rèn)證服務(wù)器將用戶導(dǎo)向客戶端事先指定的"重定向URI"(redirection URI),同時附上一個授權(quán)碼。
(D)客戶端收到授權(quán)碼,附上早先的"重定向URI",向認(rèn)證服務(wù)器申請令牌。這一步是在客戶端的后臺的服務(wù)器上完成的,對用戶不可見。
(E)認(rèn)證服務(wù)器核對了授權(quán)碼和重定向URI,確認(rèn)無誤后,向客戶端發(fā)送訪問令牌(access token)和更新令牌(refresh token)。
6.2 客戶端模式(Client Credentials Grant)
指客戶端以自己的名義,而不是以用戶的名義,向"服務(wù)提供商"進(jìn)行認(rèn)證。嚴(yán)格地說,客戶端模式并不屬于OAuth框架所要解決的問題。在這種模式中,用戶直接向客戶端注冊,客戶端以自己的名義要求"服務(wù)提供商"提供服務(wù),其實不存在授權(quán)問題。

它的步驟如下:
(A)客戶端向認(rèn)證服務(wù)器進(jìn)行身份認(rèn)證,并要求一個訪問令牌。
(B)認(rèn)證服務(wù)器確認(rèn)無誤后,向客戶端提供訪問令牌。
7 JWT
API網(wǎng)關(guān)認(rèn)證鑒權(quán)中的 Token 是采用 JWT 實現(xiàn)
7.1 JWT 原理
JSON Web Token(JWT)是目前最流行的跨域身份驗證解決方案。
傳統(tǒng)的身份驗證
1.用戶向服務(wù)器發(fā)送用戶名和密碼。
2.驗證服務(wù)器后,相關(guān)數(shù)據(jù)(如用戶角色,登錄時間等)將保存在當(dāng)前會話中。
3.服務(wù)器向用戶返回session_id,session信息都會寫入到用戶的Cookie。
4.用戶的每個后續(xù)請求都將通過在Cookie中取出session_id傳給服務(wù)器。
5.服務(wù)器收到session_id并對比之前保存的數(shù)據(jù),確認(rèn)用戶的身份。
這種模式最大的問題是,沒有分布式架構(gòu),無法支持橫向擴(kuò)展。如果使用一個服務(wù)器,該模式完全沒有問題。但是,如果它是服務(wù)器群集或面向服務(wù)的跨域體系結(jié)構(gòu)的話,則需要一個統(tǒng)一的session數(shù)據(jù)庫庫來保存會話數(shù)據(jù)實現(xiàn)共享,如果保存session的緩沖數(shù)據(jù)庫掛掉,整個認(rèn)證體系都會掛掉。
JWT 的身份驗證
JWT的原則是在服務(wù)器身份驗證之后,將生成一個JSON對象并將其發(fā)送回用戶
之后,當(dāng)用戶與服務(wù)器通信時,客戶在請求中帶上JSON對象。服務(wù)器僅依賴于這個JSON對象來標(biāo)識用戶。為了防止用戶篡改數(shù)據(jù),服務(wù)器將在生成對象時添加簽名。
服務(wù)器不保存任何會話數(shù)據(jù),即服務(wù)器變?yōu)闊o狀態(tài),使其更容易擴(kuò)展。
7.2 JWT的數(shù)據(jù)結(jié)構(gòu)
JWT的三個部分如下。JWT頭、有效載荷和簽名

頭部保存了簽名算法、令牌類型(JWT),最后,使用Base64 URL算法將上述JSON對象轉(zhuǎn)換為字符串保存。
有效載荷部分,是JWT的主體內(nèi)容部分,也是一個JSON對象,包含需要傳遞的數(shù)據(jù),JSON對象也使用Base64 URL算法轉(zhuǎn)換為字符串保存。默認(rèn)情況下是未加密的,API網(wǎng)關(guān)保存的clientId就存在于這一部分中
簽名哈希部分是對上面兩部分?jǐn)?shù)據(jù)簽名,通過指定的算法生成哈希,以確保數(shù)據(jù)不會被篡改。使用標(biāo)頭中指定的簽名算法(默認(rèn)情況下為HMAC SHA256)通過私鑰簽名。
在計算出簽名哈希后,JWT頭,有效載荷和簽名哈希的三個部分組合成一個字符串,每個部分用"."分隔,就構(gòu)成整個JWT對象。