2022-01-02面向?qū)ο?OOA网棍、OOD、OOP實(shí)戰(zhàn)總結(jié)

背景

假設(shè)薪韩,你正在參與開發(fā)一個(gè)微服務(wù)确沸。微服務(wù)通過 HTTP 協(xié)議暴露接口給其他系統(tǒng)調(diào)用,說直白點(diǎn)就是俘陷,其他系統(tǒng)通過 URL 來調(diào)用微服務(wù)的接口罗捎。有一天,你的 leader 找到你說拉盾,“為了保證接口調(diào)用的安全性桨菜,我們希望設(shè)計(jì)實(shí)現(xiàn)一個(gè)接口調(diào)用鑒權(quán)功能,只有經(jīng)過認(rèn)證之后的系統(tǒng)才能調(diào)用我們的接口捉偏,沒有認(rèn)證過的系統(tǒng)調(diào)用我們的接口會被拒絕倒得。我希望由你來負(fù)責(zé)這個(gè)任務(wù)的開發(fā),爭取盡快上線夭禽。這時(shí)候如何做霞掺?

面向?qū)ο蠓治?OOA)

需求分析的整個(gè)思考過程,從最粗糙讹躯、最模糊的需求開始菩彬,通過“提出問題 - 解決問題”的方式,循序漸進(jìn)地進(jìn)行優(yōu)化潮梯,最后得到一個(gè)足夠清晰骗灶、可落地的需求描述。

針對鑒權(quán)這個(gè)功能的開發(fā)秉馏,我們該如何做需求分析耙旦?實(shí)際上,這跟做算法題類似萝究,先從最簡單的方案想起免都,然后再優(yōu)化锉罐。所以,我把整個(gè)的分析過程分為了循序漸進(jìn)的四輪琴昆。每一輪都是對上一輪的迭代優(yōu)化氓鄙,最后形成一個(gè)可執(zhí)行、可落地的需求列表业舍。

  1. 第一輪基礎(chǔ)分析

    對于如何做鑒權(quán)這樣一個(gè)問題抖拦,最簡單的解決方案就是,通過用戶名加密碼來做認(rèn)證舷暮。我們給每個(gè)允許訪問我們服務(wù)的調(diào)用方态罪,派發(fā)一個(gè)應(yīng)用名(或者叫應(yīng)用 ID、AppID)和一個(gè)對應(yīng)的密碼(或者叫秘鑰)下面。調(diào)用方每次進(jìn)行接口請求的時(shí)候复颈,都攜帶自己的 AppID 和密碼。微服務(wù)在接收到接口調(diào)用請求之后沥割,會解析出 AppID 和密碼耗啦,跟存儲在微服務(wù)端的 AppID 和密碼進(jìn)行比對。如果一致机杜,說明認(rèn)證成功帜讲,則允許接口調(diào)用請求;否則椒拗,就拒絕接口調(diào)用請求似将。

  2. 第二輪分析優(yōu)化

    不過,這樣的驗(yàn)證方式蚀苛,每次都要明文傳輸密碼在验。密碼很容易被截獲,是不安全的堵未。那如果我們借助加密算法(比如 SHA)腋舌,對密碼進(jìn)行加密之后,再傳遞到微服務(wù)端驗(yàn)證渗蟹,是不是就可以了呢侦厚?實(shí)際上,這樣也是不安全的拙徽,因?yàn)榧用苤蟮拿艽a及 AppID,照樣可以被未認(rèn)證系統(tǒng)(或者說黑客)截獲诗宣,未認(rèn)證系統(tǒng)可以攜帶這個(gè)加密之后的密碼以及對應(yīng)的 AppID膘怕,偽裝成已認(rèn)證系統(tǒng)來訪問我們的接口。這就是典型的“重放攻擊”召庞。

    提出問題岛心,然后再解決問題来破,是一個(gè)非常好的迭代優(yōu)化方法。對于剛剛這個(gè)問題忘古,我們可以借助 OAuth 的驗(yàn)證思路來解決徘禁。調(diào)用方將請求接口的 URL 跟 AppID、密碼拼接在一起髓堪,然后進(jìn)行加密送朱,生成一個(gè) token。調(diào)用方在進(jìn)行接口請求的的時(shí)候干旁,將這個(gè) token 及 AppID驶沼,隨 URL 一塊傳遞給微服務(wù)端。微服務(wù)端接收到這些數(shù)據(jù)之后争群,根據(jù) AppID 從數(shù)據(jù)庫中取出對應(yīng)的密碼回怜,并通過同樣的 token 生成算法,生成另外一個(gè) token换薄。用這個(gè)新生成的 token 跟調(diào)用方傳遞過來的 token 對比玉雾。如果一致,則允許接口調(diào)用請求轻要;否則复旬,就拒絕接口調(diào)用請求。

  3. 第三輪分析優(yōu)化

    不過伦腐,這樣的設(shè)計(jì)仍然存在重放攻擊的風(fēng)險(xiǎn)赢底,還是不夠安全。每個(gè) URL 拼接上 AppID柏蘑、密碼生成的 token 都是固定的幸冻。未認(rèn)證系統(tǒng)截獲 URL、token 和 AppID 之后咳焚,還是可以通過重放攻擊的方式洽损,偽裝成認(rèn)證系統(tǒng),調(diào)用這個(gè) URL 對應(yīng)的接口革半。為了解決這個(gè)問題碑定,我們可以進(jìn)一步優(yōu)化 token 生成算法,引入一個(gè)隨機(jī)變量又官,讓每次接口請求生成的 token 都不一樣延刘。我們可以選擇時(shí)間戳作為隨機(jī)變量。原來的 token 是對 URL六敬、AppID碘赖、密碼三者進(jìn)行加密生成的,現(xiàn)在我們將 URL、AppID普泡、密碼播掷、時(shí)間戳四者進(jìn)行加密來生成 token。調(diào)用方在進(jìn)行接口請求的時(shí)候撼班,將 token歧匈、AppID、時(shí)間戳砰嘁,隨 URL 一并傳遞給微服務(wù)端件炉。微服務(wù)端在收到這些數(shù)據(jù)之后,會驗(yàn)證當(dāng)前時(shí)間戳跟傳遞過來的時(shí)間戳般码,是否在一定的時(shí)間窗口內(nèi)(比如一分鐘)妻率。如果超過一分鐘,則判定 token 過期板祝,拒絕接口請求宫静。如果沒有超過一分鐘,則說明 token 沒有過期券时,就再通過同樣的 token 生成算法孤里,在服務(wù)端生成新的 token,與調(diào)用方傳遞過來的 token 比對橘洞,看是否一致捌袜。如果一致,則允許接口調(diào)用請求炸枣;否則虏等,就拒絕接口調(diào)用請求。

  4. 第四輪分析優(yōu)化

    不過适肠,你可能會說霍衫,這樣還是不夠安全啊。未認(rèn)證系統(tǒng)還是可以在這一分鐘的 token 失效窗口內(nèi)侯养,通過截獲請求敦跌、重放請求,來調(diào)用我們的接口肮淇柠傍!你說得沒錯(cuò)。不過辩稽,攻與防之間惧笛,本來就沒有絕對的安全。我們能做的就是逞泄,盡量提高攻擊的成本徐紧。這個(gè)方案雖然還有漏洞静檬,但是實(shí)現(xiàn)起來足夠簡單,而且不會過度影響接口本身的性能(比如響應(yīng)時(shí)間)并级。所以,權(quán)衡安全性侮腹、開發(fā)成本嘲碧、對系統(tǒng)性能的影響,這個(gè)方案算是比較折中父阻、比較合理的了愈涩。實(shí)際上,還有一個(gè)細(xì)節(jié)我們沒有考慮到加矛,那就是履婉,如何在微服務(wù)端存儲每個(gè)授權(quán)調(diào)用方的 AppID 和密碼。當(dāng)然斟览,這個(gè)問題并不難毁腿。最容易想到的方案就是存儲到數(shù)據(jù)庫里,比如 MySQL苛茂。不過已烤,開發(fā)像鑒權(quán)這樣的非業(yè)務(wù)功能,最好不要與具體的第三方系統(tǒng)有過度的耦合妓羊。針對 AppID 和密碼的存儲胯究,我們最好能靈活地支持各種不同的存儲方式,比如 ZooKeeper躁绸、本地配置文件裕循、自研配置中心、MySQL净刮、Redis 等剥哑。我們不一定針對每種存儲方式都去做代碼實(shí)現(xiàn),但起碼要留有擴(kuò)展點(diǎn)庭瑰,保證系統(tǒng)有足夠的靈活性和擴(kuò)展性星持,能夠在我們切換存儲方式的時(shí)候,盡可能地減少代碼的改動弹灭。

  5. 最終確定需求

    調(diào)用方進(jìn)行接口請求的時(shí)候督暂,將 URL、AppID穷吮、密碼逻翁、時(shí)間戳拼接在一起,通過加密算法生成 token捡鱼,并且將 token八回、AppID、時(shí)間戳拼接在 URL 中,一并發(fā)送到微服務(wù)端缠诅。微服務(wù)端在接收到調(diào)用方的接口請求之后溶浴,從請求中拆解出 token、AppID管引、時(shí)間戳士败。微服務(wù)端首先檢查傳遞過來的時(shí)間戳跟當(dāng)前時(shí)間,是否在 token 失效時(shí)間窗口內(nèi)褥伴。如果已經(jīng)超過失效時(shí)間谅将,那就算接口調(diào)用鑒權(quán)失敗,拒絕接口調(diào)用請求重慢。如果 token 驗(yàn)證沒有過期失效饥臂,微服務(wù)端再從自己的存儲中,取出 AppID 對應(yīng)的密碼似踱,通過同樣的 token 生成算法隅熙,生成另外一個(gè) token,與調(diào)用方傳遞過來的 token 進(jìn)行匹配屯援;如果一致猛们,則鑒權(quán)成功,允許接口調(diào)用狞洋,否則就拒絕接口調(diào)用弯淘。

注意事項(xiàng)

針對框架、類庫吉懊、組件等非業(yè)務(wù)系統(tǒng)的開發(fā)庐橙,其中一個(gè)比較大的難點(diǎn)就是,需求一般都比較抽象借嗽、模糊态鳖,需要你自己去挖掘,做合理取舍恶导、權(quán)衡浆竭、假設(shè),把抽象的問題具象化惨寿,最終產(chǎn)生清晰的邦泄、可落地的需求定義。需求定義是否清晰裂垦、合理顺囊,直接影響了后續(xù)的設(shè)計(jì)、編碼實(shí)現(xiàn)是否順暢蕉拢。所以特碳,作為程序員诚亚,你一定不要只關(guān)心設(shè)計(jì)與實(shí)現(xiàn),前期的需求分析同等重要午乓。需求分析的過程實(shí)際上是一個(gè)不斷迭代優(yōu)化的過程站宗。我們不要試圖一下就能給出一個(gè)完美的解決方案,而是先給出一個(gè)粗糙的益愈、基礎(chǔ)的方案份乒,有一個(gè)迭代的基礎(chǔ),然后再慢慢優(yōu)化腕唧,這樣一個(gè)思考過程能讓我們擺脫無從下手的窘境。

面向?qū)ο笤O(shè)計(jì)(OOD)

在面向?qū)ο笤O(shè)計(jì)這一環(huán)節(jié)中瘾英,我們將需求描述轉(zhuǎn)化為具體的類的設(shè)計(jì)枣接。這個(gè)環(huán)節(jié)的工作可以拆分為下面四個(gè)部分。

  1. 劃分職責(zé)進(jìn)而識別出有哪些類缺谴;方法一:根據(jù)需求描述但惶,我們把其中涉及的功能點(diǎn),一個(gè)一個(gè)羅列出來湿蛔,然后再去看哪些功能點(diǎn)職責(zé)相近膀曾,操作同樣的屬性,可否歸為同一個(gè)類阳啥。方法二:根據(jù)需求描述:把其中的名詞羅列出來添谊,作為可能的候選類,然后再進(jìn)行篩選察迟,對于沒有經(jīng)驗(yàn)的初學(xué)者來說斩狱,這個(gè)方法比較簡單、明確扎瓶,可以直接照著做所踊。

    首先,我們要做的是逐句閱讀上面的需求描述概荷,拆解成小的功能點(diǎn)秕岛,一條一條羅列下來。注意误证,拆解出來的每個(gè)功能點(diǎn)要盡可能的小继薛。每個(gè)功能點(diǎn)只負(fù)責(zé)做一件很小的事情(專業(yè)叫法是“單一職責(zé)”)。下面是我逐句拆解上述需求描述之后雷厂,得到的功能點(diǎn)列表:1惋增、把 URL、AppID改鲫、密碼诈皿、時(shí)間戳拼接為一個(gè)字符串林束;2、對字符串通過加密算法加密生成 token稽亏;3壶冒、將 token、AppID截歉、時(shí)間戳拼接到 URL 中胖腾,形成新的 URL;4瘪松、解析 URL咸作,得到 token、AppID宵睦、時(shí)間戳等信息记罚;5、從存儲中取出 AppID 和對應(yīng)的密碼壳嚎;6桐智、根據(jù)時(shí)間戳判斷 token 是否過期失效;7烟馅、驗(yàn)證兩個(gè) token 是否匹配说庭;從上面的功能列表中,我們發(fā)現(xiàn)郑趁,1刊驴、2、6穿撮、7 都是跟 token 有關(guān)缺脉,負(fù)責(zé) token 的生成、驗(yàn)證悦穿;3攻礼、4 都是在處理 URL,負(fù)責(zé) URL 的拼接栗柒、解析礁扮;5 是操作 AppID 和密碼,負(fù)責(zé)從存儲中讀取 AppID 和密碼瞬沦。所以太伊,我們可以粗略地得到三個(gè)核心的類:AuthToken、Url逛钻、CredentialStorage僚焦。AuthToken 負(fù)責(zé)實(shí)現(xiàn) 1、2曙痘、6芳悲、7 這四個(gè)操作立肘;Url 負(fù)責(zé) 3、4 兩個(gè)操作名扛;CredentialStorage 負(fù)責(zé) 5 這個(gè)操作谅年。當(dāng)然,這是一個(gè)初步的類的劃分肮韧,其他一些不重要的融蹂、邊邊角角的類,我們可能暫時(shí)沒法一下子想全弄企,但這也沒關(guān)系超燃,面向?qū)ο蠓治觥⒃O(shè)計(jì)拘领、編程本來就是一個(gè)循環(huán)迭代淋纲、不斷優(yōu)化的過程。根據(jù)需求院究,我們先給出一個(gè)粗糙版本的設(shè)計(jì)方案,然后基于這樣一個(gè)基礎(chǔ)本涕,再去迭代優(yōu)化业汰,會更加容易一些,思路也會更加清晰一些菩颖。不過样漆,我還要再強(qiáng)調(diào)一點(diǎn),接口調(diào)用鑒權(quán)這個(gè)開發(fā)需求比較簡單晦闰,所以放祟,需求對應(yīng)的面向?qū)ο笤O(shè)計(jì)并不復(fù)雜,識別出來的類也并不多呻右。但如果我們面對的是更加大型的軟件開發(fā)跪妥、更加復(fù)雜的需求開發(fā),涉及的功能點(diǎn)可能會很多声滥,對應(yīng)的類也會比較多,像剛剛那樣根據(jù)需求逐句羅列功能點(diǎn)的方法落塑,最后會得到一個(gè)長長的列表纽疟,就會有點(diǎn)凌亂、沒有規(guī)律憾赁。針對這種復(fù)雜的需求開發(fā)污朽,我們首先要做的是進(jìn)行模塊劃分,將需求先簡單劃分成幾個(gè)小的龙考、獨(dú)立的功能模塊蟆肆,然后再在模塊內(nèi)部矾睦,應(yīng)用我們剛剛講的方法,進(jìn)行面向?qū)ο笤O(shè)計(jì)颓芭。而模塊的劃分和識別顷锰,跟類的劃分和識別,是類似的套路亡问。

  2. 定義類及其屬性和方法 我們識別出需求描述中的動詞官紫,作為候選的方法,再進(jìn)一步過濾篩選出真正的方法州藕,把功能點(diǎn)中涉及的名詞束世,作為候選屬性,然后同樣再進(jìn)行過濾篩選床玻。

    通過分析需求描述毁涉,識別出了三個(gè)核心的類,它們分別是 AuthToken锈死、Url 和 CredentialStorage∑堆撸現(xiàn)在我們來看下,每個(gè)類都有哪些屬性和方法待牵。我們還是從功能點(diǎn)列表中挖掘其屏。AuthToken 類相關(guān)的功能點(diǎn)有四個(gè):把 URL、AppID缨该、密碼偎行、時(shí)間戳拼接為一個(gè)字符串;對字符串通過加密算法加密生成 token贰拿;根據(jù)時(shí)間戳判斷 token 是否過期失效蛤袒;驗(yàn)證兩個(gè) token 是否匹配。對于方法的識別膨更,很多面向?qū)ο笙嚓P(guān)的書籍妙真,一般都是這么講的,識別出需求描述中的動詞荚守,作為候選的方法隐孽,再進(jìn)一步過濾篩選。類比一下方法的識別健蕊,我們可以把功能點(diǎn)中涉及的名詞菱阵,作為候選屬性,然后同樣進(jìn)行過濾篩選缩功。我們可以借用這個(gè)思路晴及,根據(jù)功能點(diǎn)描述,識別出來 AuthToken 類的屬性和方法嫡锌,如下所示:
    [圖片上傳失敗...(image-e4415f-1641114774258)]

從上面的類圖中虑稼,我們可以發(fā)現(xiàn)這樣三個(gè)小細(xì)節(jié)琳钉。第一個(gè)細(xì)節(jié):并不是所有出現(xiàn)的名詞都被定義為類的屬性,比如 URL蛛倦、AppID歌懒、密碼、時(shí)間戳這幾個(gè)名詞溯壶,我們把它作為了方法的參數(shù)及皂。第二個(gè)細(xì)節(jié):我們還需要挖掘一些沒有出現(xiàn)在功能點(diǎn)描述中屬性,比如 createTime且改,expireTimeInterval验烧,它們用在 isExpired() 函數(shù)中,用來判定 token 是否過期又跛。第三個(gè)細(xì)節(jié):我們還給 AuthToken 類添加了一個(gè)功能點(diǎn)描述中沒有提到的方法 getToken()碍拆。第一個(gè)細(xì)節(jié)告訴我們,從業(yè)務(wù)模型上來說慨蓝,不應(yīng)該屬于這個(gè)類的屬性和方法感混,不應(yīng)該被放到這個(gè)類里。比如 URL礼烈、AppID 這些信息浩习,從業(yè)務(wù)模型上來說,不應(yīng)該屬于 AuthToken济丘,所以我們不應(yīng)該放到這個(gè)類中。第二洽蛀、第三個(gè)細(xì)節(jié)告訴我們摹迷,在設(shè)計(jì)類具有哪些屬性和方法的時(shí)候,不能單純地依賴當(dāng)下的需求郊供,還要分析這個(gè)類從業(yè)務(wù)模型上來講峡碉,理應(yīng)具有哪些屬性和方法。這樣可以一方面保證類定義的完整性驮审,另一方面不僅為當(dāng)下的需求還為未來的需求做些準(zhǔn)備鲫寄。

Url 類相關(guān)的功能點(diǎn)有兩個(gè):將 token、AppID疯淫、時(shí)間戳拼接到 URL 中地来,形成新的 URL;解析 URL熙掺,得到 token未斑、AppID、時(shí)間戳等信息币绩。雖然需求描述中蜡秽,我們都是以 URL 來代指接口請求府阀,但是,接口請求并不一定是以 URL 的形式來表達(dá)芽突,還有可能是 Dubbo试浙、RPC 等其他形式。為了讓這個(gè)類更加通用寞蚌,命名更加貼切田巴,我們接下來把它命名為 ApiRequest。下面是我根據(jù)功能點(diǎn)描述設(shè)計(jì)的 ApiRequest 類睬澡。

[圖片上傳失敗...(image-731a41-1641114774258)]
CredentialStorage 類相關(guān)的功能點(diǎn)有一個(gè):從存儲中取出 AppID 和對應(yīng)的密碼固额。CredentialStorage 類非常簡單,類圖如下所示煞聪。為了做到抽象封裝具體的存儲方式斗躏,我們將 CredentialStorage 設(shè)計(jì)成了接口,基于接口而非具體的實(shí)現(xiàn)編程昔脯。

[圖片上傳失敗...(image-f699b8-1641114774258)]

  1. 定義類與類之間的交互關(guān)系啄糙;UML 統(tǒng)一建模語言中定義了六種類之間的關(guān)系。它們分別是:泛化云稚、實(shí)現(xiàn)隧饼、關(guān)聯(lián)、聚合静陈、組合燕雁、依賴。我們從更加貼近編程的角度鲸拥,對類與類之間的關(guān)系做了調(diào)整拐格,保留四個(gè)關(guān)系:泛化(繼承)、實(shí)現(xiàn)(接口)刑赶、組合捏浊、依賴

泛化(Generalization)可以簡單理解為繼承關(guān)系撞叨。具體到 Java 代碼就是下面這樣:

public class A { ... }
public class B extends A { ... }

實(shí)現(xiàn)(Realization)一般是指接口和實(shí)現(xiàn)類之間的關(guān)系金踪。具體到 Java 代碼就是下面這樣:

    public interface A {...}
    public class B implements A { ... }

組合(Composition)關(guān)系替代 UML 中組合、聚合牵敷、關(guān)聯(lián)三個(gè)概念胡岔。只要 B 類對象是 A 類對象的成員變量,那我們就稱枷餐,A 類跟 B 類是組合關(guān)系姐军,比如鳥與翅膀之間的關(guān)系。具體到 Java 代碼就是下面這樣:

    public class A {
     private B b;
     public A() {
     this.b = new B();
     }
    }

依賴(Dependency)是一種比關(guān)聯(lián)關(guān)系更加弱的關(guān)系,包含關(guān)聯(lián)關(guān)系奕锌。不管是 B 類對象是 A 類對象的成員變量著觉,還是 A 類的方法使用 B 類對象作為參數(shù)或者返回值、局部變量惊暴,只要 B 類對象和 A 類對象有任何使用關(guān)系饼丘,我們都稱它們有依賴關(guān)系。具體到 Java 代碼就是下面這樣:

```
public class A {
 private B b;
 public A(B b) {
 this.b = b;
 }
}
或者
public class A {
 private B b;
 public A() {
 this.b = new B();
 }
}
或者
public class A {
 public void func(B b) { ... }
}

剛剛我們定義的類之間都有哪些關(guān)系呢辽话?因?yàn)槟壳爸挥腥齻€(gè)核心的類肄鸽,所以只用到了實(shí)現(xiàn)關(guān)系,也即 CredentialStorage 和 MysqlCredentialStorage 之間的關(guān)系油啤。接下來講到組裝類的時(shí)候典徘,我們還會用到依賴關(guān)系、組合關(guān)系益咬,但是泛化關(guān)系暫時(shí)沒有用到逮诲。

  1. 將類組裝起來并提供執(zhí)行入口我們要將所有的類組裝在一起,提供一個(gè)執(zhí)行入口幽告。這個(gè)入口可能是一個(gè) main() 函數(shù)梅鹦,也可能是一組給外部用的 API 接口。通過這個(gè)入口冗锁,我們能觸發(fā)整個(gè)代碼跑起來齐唆。

類定義好了,類之間必要的交互關(guān)系也設(shè)計(jì)好了冻河,接下來我們要將所有的類組裝在一起箍邮,提供一個(gè)執(zhí)行入口。這個(gè)入口可能是一個(gè) main() 函數(shù)叨叙,也可能是一組給外部用的 API 接口锭弊。通過這個(gè)入口,我們能觸發(fā)整個(gè)代碼跑起來摔敛。接口鑒權(quán)并不是一個(gè)獨(dú)立運(yùn)行的系統(tǒng),而是一個(gè)集成在系統(tǒng)上運(yùn)行的組件全封,所以马昙,我們封裝所有的實(shí)現(xiàn)細(xì)節(jié),設(shè)計(jì)了一個(gè)最頂層的 ApiAuthenticator 接口類刹悴,暴露一組給外部調(diào)用者使用的 API 接口行楞,作為觸發(fā)執(zhí)行鑒權(quán)邏輯的入口。具體的類的設(shè)計(jì)如下所示:

面向?qū)ο缶幊?OOP)

面向?qū)ο笤O(shè)計(jì)完成之后土匀,我們已經(jīng)定義清晰了類子房、屬性岖寞、方法迷守、類之間的交互,并且將所有的類組裝起來,提供了統(tǒng)一的執(zhí)行入口勉耀。接下來,面向?qū)ο缶幊痰墓ぷ髦杷兀褪菍⑦@些設(shè)計(jì)思路翻譯成代碼實(shí)現(xiàn)侠鳄。

/**
* ApiAuthenticator接口--執(zhí)行入口
*
*/
public interface ApiAuthenticator {

void auth(String url);

void auth(ApiRequest apiRequest);
}

@Getter
public class ApiRequest {

private String appId;

private String token;

private String originalUrl;

private Long timestamp = System.currentTimeMillis();

public ApiRequest(String appId, String token, Long timestamp,String originalUrl) {
this.appId = appId;
this.token = token;
this.timestamp = timestamp;
this.originalUrl = originalUrl;
}

public static ApiRequest buildFromUrl(String url) {
String[] split = url.split("&");
return new ApiRequest(split[0],split[1],Long.valueOf(split[2]),url);
}
}

public class DefaultApiAuthenticatorImpl implements ApiAuthenticator {

@Autowired
private CredentialStorage credentialStorage;


@Override
public void auth(String url) {
ApiRequest apiRequest = ApiRequest.buildFromUrl(url);
auth(apiRequest);
}

@Override
public void auth(ApiRequest apiRequest) {
String appId = apiRequest.getAppId();
String token = apiRequest.getToken();
Long timestamp = apiRequest.getTimestamp();
String originalUrl = apiRequest.getOriginalUrl();

AuthToken clientAuthToken = new AuthToken(token, timestamp);
if (clientAuthToken.isExpired()){
throw new RuntimeException("Token is expired.");
}

String password = credentialStorage.getPasswordByAppId(appId);

AuthToken serverAuthToken = AuthToken.generate(originalUrl, appId, password, timestamp);
if (!serverAuthToken.match(clientAuthToken)) {
throw new RuntimeException("Token verfication failed.");
}
}
}

@Getter
public class AuthToken {

private static final long DEFAULT_TIMEOUT = 60 * 1000l;

private String token;

private Long timestamp;

private boolean isExpired = true;

public AuthToken(String token, Long timestamp) {

this.token = token;
this.timestamp = timestamp;
if(System.currentTimeMillis() - timestamp < DEFAULT_TIMEOUT){
this.isExpired = false;
}
}

public static AuthToken generate(String originalUrl, String appId, String password, Long timestamp) {
String token = null;
Base64.Encoder encoder = Base64.getUrlEncoder();
try {
byte[] bytes = String.join("&", originalUrl, appId, password, String.valueOf(timestamp)).getBytes("UTF-8");
token = new String(encoder.encode(bytes),"UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}

return new AuthToken(token,timestamp);
}

public boolean match(AuthToken clientAuthToken) {

return token.equals(clientAuthToken);
}
}

public interface CredentialStorage {
/**
* 根據(jù)AppID獲取密碼
*
* todo 數(shù)據(jù)可以存儲在本地磁盤,本地內(nèi)存送讲、內(nèi)存型數(shù)據(jù)庫奸笤、關(guān)系型數(shù)據(jù)庫,所以此處有接口
*
* @param appId
* @return
*/
String getPasswordByAppId(String appId);
}

總結(jié)

  • 面向?qū)ο蠓治龅漠a(chǎn)出是詳細(xì)的需求描述;

  • 面向?qū)ο笤O(shè)計(jì)的產(chǎn)出是類哼鬓;

  • 面向?qū)ο缶幊淌菍⒎治龊驮O(shè)計(jì)的結(jié)果進(jìn)行實(shí)現(xiàn)监右;

在之前的講解中,面向?qū)ο蠓治鲆煜!⒃O(shè)計(jì)健盒、實(shí)現(xiàn),每個(gè)環(huán)節(jié)的界限劃分都比較清楚宠互。而且味榛,設(shè)計(jì)和實(shí)現(xiàn)基本上是按照功能點(diǎn)的描述,逐句照著翻譯過來的予跌。這樣做的好處是先做什么搏色、后做什么,非常清晰券册、明確频轿,有章可循,即便是沒有太多設(shè)計(jì)經(jīng)驗(yàn)的初級工程師烁焙,都可以按部就班地參照著這個(gè)流程來做分析航邢、設(shè)計(jì)和實(shí)現(xiàn)。不過骄蝇,在平時(shí)的工作中膳殷,大部分程序員往往都是在腦子里或者草紙上完成面向?qū)ο蠓治龊驮O(shè)計(jì),然后就開始寫代碼了九火,邊寫邊思考邊重構(gòu)赚窃,并不會嚴(yán)格地按照剛剛的流程來執(zhí)行。而且岔激,說實(shí)話勒极,即便我們在寫代碼之前,花很多時(shí)間做分析和設(shè)計(jì)虑鼎,繪制出完美的類圖辱匿、UML 圖键痛,也不可能把每個(gè)細(xì)節(jié)、交互都想得很清楚匾七。在落實(shí)到代碼的時(shí)候絮短,我們還是要反復(fù)迭代、重構(gòu)乐尊、打破重寫戚丸。畢竟,整個(gè)軟件開發(fā)本來就是一個(gè)迭代扔嵌、修修補(bǔ)補(bǔ)限府、遇到問題解決問題的過程,是一個(gè)不斷重構(gòu)的過程痢缎。我們沒法嚴(yán)格地按照順序執(zhí)行各個(gè)步驟胁勺。這就類似你去學(xué)駕照,駕校教的都是比較正規(guī)的流程独旷,先做什么署穗,后做什么,你只要照著做就能順利倒車入庫嵌洼,但實(shí)際上案疲,等你開熟練了,倒車入庫很多時(shí)候靠的都是經(jīng)驗(yàn)和感覺麻养。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載褐啡,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末鳖昌,一起剝皮案震驚了整個(gè)濱河市备畦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌许昨,老刑警劉巖懂盐,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異糕档,居然都是意外死亡莉恼,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門速那,熙熙樓的掌柜王于貴愁眉苦臉地迎上來俐银,“玉大人,你說我怎么就攤上這事琅坡∠せ迹” “怎么了残家?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵榆俺,是天一觀的道長。 經(jīng)常有香客問我,道長茴晋,這世上最難降的妖魔是什么陪捷? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮诺擅,結(jié)果婚禮上市袖,老公的妹妹穿的比我還像新娘。我一直安慰自己烁涌,他們只是感情好苍碟,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著撮执,像睡著了一般微峰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上抒钱,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天蜓肆,我揣著相機(jī)與錄音,去河邊找鬼谋币。 笑死仗扬,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蕾额。 我是一名探鬼主播早芭,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼凡简!你這毒婦竟也來了逼友?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤秤涩,失蹤者是張志新(化名)和其女友劉穎帜乞,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體筐眷,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡黎烈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了匀谣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片照棋。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖武翎,靈堂內(nèi)的尸體忽然破棺而出烈炭,到底是詐尸還是另有隱情,我是刑警寧澤宝恶,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布符隙,位于F島的核電站趴捅,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏霹疫。R本人自食惡果不足惜拱绑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望丽蝎。 院中可真熱鬧猎拨,春花似錦、人聲如沸屠阻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽国觉。三九已至类腮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蛉加,已是汗流浹背蚜枢。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留针饥,地道東北人厂抽。 一個(gè)月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像丁眼,于是被迫代替她去往敵國和親筷凤。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容