即刻時間——設計模式之美 實戰二(上):如何對介面鑑權這樣一個功能開發做物件導向分析?

小豬快跑22發表於2020-11-30

物件導向分析(OOA)、物件導向設計(OOD)、物件導向程式設計(OOP),是物件導向開發的三個主要環節。在前面的章節中,我對三者的講解比較偏理論、偏概括性,目的是讓你先有一個巨集觀的瞭解,知道什麼是 OOA、OOD、OOP。不過,光知道“是什麼”是不夠的,我們更重要的還是要知道“如何做”,也就是,如何進行物件導向分析、設計與程式設計。

在過往的工作中,我發現,很多工程師,特別是初級工程師,本身沒有太多的專案經驗,或者參與的專案都是基於開發框架填寫 CRUD 模板似的程式碼,導致分析、設計能力比較欠缺。當他們拿到一個比較籠統的開發需求的時候,往往不知道從何入手。

對於“如何做需求分析,如何做職責劃分?需要定義哪些類?每個類應該具有哪些屬性、方法?類與類之間該如何互動?如何組裝類成一個可執行的程式?”等等諸多問題,都沒有清晰的思路,更別提利用成熟的設計原則、思想或者設計模式,開發出具有高內聚低耦合、易擴充套件、易讀等優秀特性的程式碼了。

所以,我打算用兩節課的時間,結合一個真實的開發案例,從基礎的需求分析、職責劃分、類的定義、互動、組裝執行講起,將最基礎的物件導向分析、設計、程式設計的套路給你講清楚,為後面學習設計原則、設計模式打好基礎。

話不多說,讓我們正式開始今天的學習吧!

案例介紹和難點剖析

假設,你正在參與開發一個微服務。微服務通過 HTTP 協議暴露介面給其他系統呼叫,說直白點就是,其他系統通過 URL 來呼叫微服務的介面。有一天,你的 leader 找到你說,“為了保證介面呼叫的安全性,我們希望設計實現一個介面呼叫鑑權功能,只有經過認證之後的系統才能呼叫我們的介面,沒有認證過的系統呼叫我們的介面會被拒絕。我希望由你來負責這個任務的開發,爭取儘快上線。”

leader 丟下這些話就走了。這個時候,你該如何來做呢?有沒有腦子裡一團漿糊,一時間無從下手的感覺呢?為什麼會有這種感覺呢?我個人覺得主要有下面兩點原因。

1. 需求不明確

leader 給到的需求過於模糊、籠統,不夠具體、細化,離落地到設計、編碼還有一定的距離。而人的大腦不擅長思考這種過於抽象的問題。這也是真實的軟體開發區別於應試教育的地方。應試教育中的考試題目,一般都是一個非常具體的問題,我們去解答就好了。而真實的軟體開發中,需求幾乎都不是很明確。

我們前面講過,物件導向分析主要的分析物件是“需求”,因此,物件導向分析可以粗略地看成“需求分析”。實際上,不管是需求分析還是物件導向分析,我們首先要做的都是將籠統的需求細化到足夠清晰、可執行。我們需要通過溝通、挖掘、分析、假設、梳理,搞清楚具體的需求有哪些,哪些是現在要做的,哪些是未來可能要做的,哪些是不用考慮做的。

2. 缺少鍛鍊

相比單純的業務 CRUD 開發,鑑權這個開發任務,要更有難度。鑑權作為一個跟具體業務無關的功能,我們完全可以把它開發成一個獨立的框架,整合到很多業務系統中。而作為被很多系統複用的通用框架,比起普通的業務程式碼,我們對框架的程式碼質量要求要更高。

開發這樣通用的框架,對工程師的需求分析能力、設計能力、編碼能力,甚至邏輯思維能力的要求,都是比較高的。如果你平時做的都是簡單的 CRUD 業務開發,那這方面的鍛鍊肯定不會很多,所以,一旦遇到這種開發需求,很容易因為缺少鍛鍊,腦子放空,不知道從何入手,完全沒有思路。

對案例進行需求分析

實際上,需求分析的工作很瑣碎,也沒有太多固定的章法可尋,所以,我不打算很牽強地羅列那些聽著有用、實際沒用的方法論,而是希望通過鑑權這個例子,來給你展示一下,面對需求分析的時候,我的完整的思考路徑是什麼樣的。希望你能自己去體會,舉一反三地類比應用到其他專案的需求分析中。

儘管針對框架、元件、類庫等非業務系統的開發,我們一定要有元件化意識、框架意識、抽象意識,開發出來的東西要足夠通用,不能侷限於單一的某個業務需求,但這並不代表我們就可以脫離具體的應用場景,悶頭拍腦袋做需求分析。多跟業務團隊聊聊天,甚至自己去參與幾個業務系統的開發,只有這樣,我們才能真正知道業務系統的痛點,才能分析出最有價值的需求。不過,針對鑑權這一功能的開發,最大的需求方還是我們自己,所以,我們也可以先從滿足我們自己系統的需求開始,然後再迭代優化。

現在,我們來看一下,針對鑑權這個功能的開發,我們該如何做需求分析?

實際上,這跟做演算法題類似,先從最簡單的方案想起,然後再優化。所以,我把整個的分析過程分為了循序漸進的四輪。每一輪都是對上一輪的迭代優化,最後形成一個可執行、可落地的需求列表。

1. 第一輪基礎分析

對於如何做鑑權這樣一個問題,最簡單的解決方案就是,通過使用者名稱加密碼來做認證。我們給每個允許訪問我們服務的呼叫方,派發一個應用名(或者叫應用 ID、AppID)和一個對應的密碼(或者叫祕鑰)。呼叫方每次進行介面請求的時候,都攜帶自己的 AppID 和密碼。微服務在接收到介面呼叫請求之後,會解析出 AppID 和密碼,跟儲存在微服務端的 AppID 和密碼進行比對。如果一致,說明認證成功,則允許介面呼叫請求;否則,就拒絕介面呼叫請求。

2. 第二輪分析優化

不過,這樣的驗證方式,每次都要明文傳輸密碼。密碼很容易被截獲,是不安全的。那如果我們藉助加密演算法(比如 SHA),對密碼進行加密之後,再傳遞到微服務端驗證,是不是就可以了呢?實際上,這樣也是不安全的,因為加密之後的密碼及 AppID,照樣可以被未認證系統(或者說黑客)截獲,未認證系統可以攜帶這個加密之後的密碼以及對應的 AppID,偽裝成已認證系統來訪問我們的介面。這就是典型的重放攻擊

提出問題,然後再解決問題,是一個非常好的迭代優化方法。對於剛剛這個問題,我們可以藉助 OAuth 的驗證思路來解決。呼叫方將請求介面的 URL 跟 AppID、密碼拼接在一起,然後進行加密,生成一個 token。呼叫方在進行介面請求的的時候,將這個 token 及 AppID,隨 URL 一塊傳遞給微服務端。微服務端接收到這些資料之後,根據 AppID 從資料庫中取出對應的密碼,並通過同樣的 token 生成演算法,生成另外一個 token。用這個新生成的 token 跟呼叫方傳遞過來的 token 對比。如果一致,則允許介面呼叫請求;否則,就拒絕介面呼叫請求。

這個方案稍微有點複雜,我畫了一張示例圖,來幫你理解整個流程。

在這裡插入圖片描述

3. 第三輪分析優化

不過,這樣的設計仍然存在重放攻擊的風險,還是不夠安全。每個 URL 拼接上 AppID、密碼生成的 token 都是固定的。未認證系統截獲 URL、token 和 AppID 之後,還是可以通過重放攻擊的方式,偽裝成認證系統,呼叫這個 URL 對應的介面。

為了解決這個問題,我們可以進一步優化 token 生成演算法,引入一個隨機變數,讓每次介面請求生成的 token 都不一樣。我們可以選擇時間戳作為隨機變數。原來的 token 是對 URL、AppID、密碼三者進行加密生成的,現在我們將 URL、AppID、密碼、時間戳四者進行加密來生成 token。呼叫方在進行介面請求的時候,將 token、AppID、時間戳,隨 URL 一併傳遞給微服務端。

微服務端在收到這些資料之後,會驗證當前時間戳跟傳遞過來的時間戳,是否在一定的時間視窗內(比如一分鐘)。如果超過一分鐘,則判定 token 過期,拒絕介面請求。如果沒有超過一分鐘,則說明 token 沒有過期,就再通過同樣的 token 生成演算法,在服務端生成新的 token,與呼叫方傳遞過來的 token 比對,看是否一致。如果一致,則允許介面呼叫請求;否則,就拒絕介面呼叫請求。

優化之後的認證流程如下圖所示。

在這裡插入圖片描述

4. 第四輪分析優化

不過,你可能會說,這樣還是不夠安全啊。未認證系統還是可以在這一分鐘的 token 失效視窗內,通過截獲請求、重放請求,來呼叫我們的介面啊!

你說得沒錯。不過,攻與防之間,本來就沒有絕對的安全。我們能做的就是,儘量提高攻擊的成本。這個方案雖然還有漏洞,但是實現起來足夠簡單,而且不會過度影響介面本身的效能(比如響應時間)。所以,權衡安全性、開發成本、對系統效能的影響,這個方案算是比較折中、比較合理的了。

實際上,還有一個細節我們沒有考慮到,那就是,如何在微服務端儲存每個授權呼叫方的 AppID 和密碼。當然,這個問題並不難。最容易想到的方案就是儲存到資料庫裡,比如 MySQL。不過,開發像鑑權這樣的非業務功能,最好不要與具體的第三方系統有過度的耦合。

針對 AppID 和密碼的儲存,我們最好能靈活地支援各種不同的儲存方式,比如 ZooKeeper、本地配置檔案、自研配置中心、MySQL、Redis 等。我們不一定針對每種儲存方式都去做程式碼實現,但起碼要留有擴充套件點,保證系統有足夠的靈活性和擴充套件性,能夠在我們切換儲存方式的時候,儘可能地減少程式碼的改動。

5. 最終確定需求

到此,需求已經足夠細化和具體了。現在,我們按照鑑權的流程,對需求再重新描述一下。如果你熟悉 UML,也可以用時序圖、流程圖來描述。不過,用什麼描述不是重點,描述清楚才是最重要的。考慮到在接下來的物件導向設計環節中,我會基於文字版本的需求描述,來進行類、屬性、方法、互動等的設計,所以,這裡我給出的最終需求描述是文字版本的。

● 呼叫方進行介面請求的時候,將 URL、AppID、密碼、時間戳拼接在一起,通過加密演算法生成 token,並且將 token、AppID、時間戳拼接在 URL 中,一併傳送到微服務端。

● 微服務端在接收到呼叫方的介面請求之後,從請求中拆解出 token、AppID、時間戳。

● 微服務端首先檢查傳遞過來的時間戳跟當前時間,是否在 token 失效時間視窗內。如果已經超過失效時間,那就算介面呼叫鑑權失敗,拒絕介面呼叫請求。

● 如果 token 驗證沒有過期失效,微服務端再從自己的儲存中,取出 AppID 對應的密碼,通過同樣的 token 生成演算法,生成另外一個 token,與呼叫方傳遞過來的 token 進行匹配;如果一致,則鑑權成功,允許介面呼叫,否則就拒絕介面呼叫。

這就是我們需求分析的整個思考過程,從最粗糙、最模糊的需求開始,通過“提出問題 - 解決問題”的方式,循序漸進地進行優化,最後得到一個足夠清晰、可落地的需求描述。

相關文章