用戶驗證設計

Avatar of 黃耀賢.
Avatar of 黃耀賢.

用戶驗證設計

軟體設計
New Taipei City, Taiwan

⽤⼾驗證設計

黃耀賢 [email protected]

⽤⼾模組是許多系統必定規劃的模式。⽤⼾有⾝份,可以登入系統,並有許多⾏為過程都根據⽤⼾⽽記錄。

本⽂件基於領域模式設計 (Modeling by Domain-driven Design) ,並著眼於函數式程式設計(Functional Programming) 。

技術盤點

承襲⾃ UML 類別圖 (Class Diagram) 與三層式架構 (Layered Architecture) ,領域驅動設計特化了 UML 類別圖並抽離出⼀種聚焦於領域的概念模型。特化 UML 類別圖的效果,⼀是將關聯線加上箭頭,使物件與物件關係⽅向明確,更容易按圖寫程式。領域模型由實體 (Entity) ,值件 (Value Object) 與服務 (Service) 為基本,以聚合 (Aggregate) 為精簡單元,使關聯線減少。並將整體範圍 (Context) 劃分為許多⼩範圍 (Sub-Context) ,領域的世界變成許多範圍界線 (Bounded Context) ,⽽為了指明,我們多⽤範圍地圖 (Context Map) 羅列領域世界。

依實體與聚合模式,看似⽤物件導向程式語⾔較容易運作,但也因此顯得由技術綁架概念。

Erlang / OTP 程式語⾔,是個函數式程式設計語⾔,並以多程序並⾏運作。程序為獨立的單位,不與其他程序共享資源,程序只能以訊息發送給另⼀程序與之溝通。 Erlang / OTP 變化為 Elixir 程式語⾔。 Elixir 程式語⾔有較多抽象,包括 Access 塑造變數賦值, Registry 提供鍵值對應與查詢, Agent 為簡單版的服務單元,⽽ GenServer 為普遍版的服務單元。

⽤物件導向程式語⾔實現領域模式設計,會經常使⽤ UML 類別圖。⽽⽤函數式程式設計語⾔實現領域模式設計,則可能使⽤ UML 順序圖 (Sequence Diagram) 或 IDEF0 模式語⾔,較為適合。

Scott Wlaschin 的 “Domain Model Made Functional” ⼀書使⽤ F# 程式語⾔與範圍地圖

(Context Map) 展⽰領域設計,並妥善放置非函數導向的模組在包含領域在內的分層架構外圍,如領域模式裡頭提到的倉儲單元 (Repository) ,在 “Domain Model Made Functional” ⼀書裡,不但擺在架構外圍,並且型態消融,不再有所謂倉儲單元。

設計基礎技術

倉儲單元 (Repository) 究竟是個什麼,在函數式程式設計的技術考量裡,是個緊要的⼯作事項。接著談資料庫的倉儲單元,與暫存表的倉儲單元。

資料庫的倉儲單元,以 Elixir ⼀套應⽤框架 Phoenix Framework 來說,其中運⽤名為 Ecto 的 ORM 框架提供資料庫存取,使資料經由存取管道以 Ecto ORM 機能從資料庫取出,⽽成為 Elixir 運作環境裡的物件,⼜由 Ecto ORM 機能使資料物件變成資料庫裡的資料更新命令,即 SQL Update 。

暫存表的倉儲單元,以 Registry 使許多程序與資料能以暫時存儲在記憶體,並以鍵值操作存放或取⽤。

Erlang / OTP 與 Elixir ⼜有個分散式資料通訊機能,長得很像所謂具名管道 (Named Pipe) :以 Phoenix Framework 裡有個稱為 Phoenix.PubSub 的東⻄,實現訊息推播機能。 Erlang / OTP 以 虛擬機 ERTS 為運作單位,⼀旦 ERTS 以分散模式啟動,即為節點。每個程序能由所在的節點,向另⼀個節點的另⼀個程序傳遞訊息。跨節點的訊息傳遞銜接點是⼀個名稱,該名稱的背後是⼀個 Phoenix.PubSub 程序。例如訊息通道的名稱為 MyApp.PubSub ,我這個節點有個程序想傳⼀則指令,則對著 MyApp.PubSub 名稱傳訊息。在此前,如果有別的節點裡頭也有個 Phoenix.PubSub 名稱為 MyApp.PubSub ,且有某個程序向該通道訂閱訊息,則此時的訊息能藉由相同的名稱 MyApp.PubSub ,使訊息經由⼆個節點,讓末端訂閱訊息通道的程序收到訊息。

⽤⼾驗證設計

⽤⼾驗證的總⽬標,是要有個函數 auth ( user Id, password ) : token ,只要輸入帳號名稱與密碼,即換到⼀個 token 。這個 token 可說是通過認證的記號 (authenticated) 也可說是獲得授權的代表 (authorized) 。

以設想系統規模可靈活化,我定義系統為三個範圍:「⽤⼾驗證」「存儲」「暫存」。普通⼩網站可能只有⽤⼾驗證與存儲⼆個範圍,甚⾄將⼆者合併為⼀個範圍。⽽在此,我系統分為三個範圍,

範圍之間以 Phoenix.PubSub 訊息通道為介⾯,使每⼀組程式可⽤函數式程式設計撰寫:有些函數主做事件包裝與傳遞,⽽有些函數主做存儲庫的控管。

訊息通道的⽤法,須明列主題與訊息。客⼾端向訊息通道訂閱⼀個主題,⽽當任何來源端發送訊

息,是向訊息通道發送「主題、訊息」⼀對資訊。主題被消化、比對,⽽訊息是最後客⼾端收到的內容。訊息概括規格如下表。

主題為了包容⼒,分為 system, domain, job 三個層次。訊息內容先將 topic 主題重複⼀次,接著有 action, arguments, return topic 三個欄位。⼀旦有⼀端需發送訊息之後並等待訊息回覆,則:

⾸先由發送端做⼀個暫時的符號,當作 return topic ,並發送端先向訊息通道訂閱 return topic ,將由 return topic 接收唯⼀次回覆訊息;接著將連同 topic, action, arguments, return topic 等欄位發訊到 topic 主題。

於是,看⽤⼾驗證範圍 (User Auth Context) 裡的函數。

⽽由⽤⼾驗證範圍傳遞訊息到暫存範圍的訊息有三種,分為下表⼆類。

在⽤⼾驗證範圍,⼀旦進⾏ auth/2 或 auth/1 或 discard/1 函數呼叫,之後會想要從⽤⼾驗證範圍向暫存範圍傳遞訊息,因為主要的調閱 token 與管理 token 都在暫存範圍進⾏。

接著看暫存範圍 (Registry Context) :

暫存範圍的任務在管理⼀個 Registry 暫存表。暫存表是鍵值對應,值可以是 Erlang Process ,並⼀旦 Erlang Process 關閉或失效了,則該鍵值立即由暫存表撤銷。於是,我可以由暫存表找⼀個 user Id 對 Erlang Process 即 user Pid 的對應紀錄,然後對該 user Pid 驗證它的密碼或 token 。

暫存範圍的函數定義有這幾個。

get/1 函數調閱 Registry ,並且可能找不到標的。⼀旦找不到標的,會由暫存範圍向存儲範圍發送訊息,內容為索取資料的根本紀錄。 get/1 也期待之後由存儲範圍回訊告知 {:ok, Id, token} 訊息,⽽可接著在 Registry 創建暫存紀錄。除此外, auth/2,1 與 discard/1 函數進⾏所需的調閱: auth/2,1 牽涉對 user Pid 的互動⾏為,⽽ discard/1 牽涉對 Registry 互動以管控暫存。

存儲範圍 (Repository Context) :

存儲範圍有個 auth/3 函數,除帳號名稱與密碼之外,第三個參數為預先計算的 token 。

如果帳號驗證通過,即從資料庫取得該⽤⼾帳號的 Id ,連同輸入參數 token ⼀起回覆給 return topic 。

以上,⽤⼾驗證設計討論完畢。

⽤⼾驗證設計回顧

回顧⽤⼾驗證的範圍地圖。⾸先,我要⽤⼾驗證入⼝在⽤⼾驗證範圍上,主要是 auth/1 ,⽽ auth/2 為初次驗證需要。驗證的第⼀關,先從暫存範圍依序確認⼆件事:⼀是在暫存表裡找得到⽤⼾ user Pid ,⼆是對 Process user Pid 能調閱結果,⼀旦調閱出結果,即回覆給⽤⼾驗證範圍的呼叫者 auth/2,1 ,完成驗證。

如果在暫存範圍無法完成驗證,例如在暫存表找不到⽤⼾紀錄,則驗證的第⼆項任務是要從資料庫驗證,並將⽤⼾紀錄從資料庫拉升到暫存表。在回覆訊息⽅⾯,由於⾸先⽤⼾驗證範圍 auth/2,1 放了⼀個 return topic 本來希望暫存範圍的函數可以找出答案,但是碰巧暫存範圍找不出答案,⽽需要存儲範圍幫忙找答案,因此當暫存範圍放出第⼆⼿訊息時,可延⽤ return topic ,等到由存儲範圍 auth/3 完成並回覆 {:ok, Id, tokken} 或 nil 訊息格式時,⽤⼾驗證範圍與暫存範圍都由同⼀ return topic 收到回覆,例如獲得 {:ok, 1024, “token-FFAE250C”} ,對⽤⼾驗證範圍 auth/2,1 來講,已得到直接可⽤的驗證結果,⽽同時,對暫存範圍的函數來講,收到未列入暫存表的 user Id ,於是可即刻新增⼀筆暫存⽤⼾紀錄。

靈活規模化

⼀旦將 Erlang / OTP 虛擬機 ERTS 啟動為多個節點,並將「⽤⼾驗證」「暫存」「存儲」等三個模組分別放置在⾄少三個節點上,將可為系統配置得更靈活。

我設想⼆種情境,討論規模化的配置。

以分散部署的基礎,每⼀份同⼀性質的模組可以部署⼀份到 N 份,例如我架構將暫存模組部署三

份,⽽存儲模組部署⼆份。相較於同⼀性質模組有多份時,容易添加⼀份部署或撤除⼀份部署,在此,我設想最單純的,當每個模組分別部署⼀份時,

⼀、為了升級改版暫存模組,⽽將暫存模組撤離。

⼆、為了維護資料庫,⽽將存儲模組撤離。

如上述⼆種情況,當⼀組緊密搭配的模組要撤下⼀部份機能時,剩餘系統的持續運作能⼒有怎樣的可能?

暫存模組撤離

⼀旦將暫存模組撤離,則每當⽤⼾驗證模組發出 auth 訊息,則無末端接應。

由於接應⽅須使⽤事件派遣模式 (Event Dispatcher) 能事先訂閱 topic 。在之前的設計,暫存範圍與存儲範圍都訂閱 topic { “my system”, “user auth”, “auth” } ,⽽⼆者差在只有暫存範圍處理 message 的 action 欄位為 “auth” 與 “discard” 的訊息,⽽存儲範圍處理 message action 欄位為 “get user” 的訊息。

解決⽅法是要求存儲範圍代管,在存儲模組裡提供接應 action 為 “auth” 或 “discard” 函數的非暫存版本,及其事件派遣程式。當要撤離暫存模組時,先在存儲模組啟動前述事件派遣,即可將暫存模組移出網路。

另⼀⾃動情境,當暫存模組偶然斷線⽽撤離時,在 Erlang / OTP 有他種處理⽅式稱為暫⾏代理

(Fail Over) ,並等之後原系統重新啟動之後,可以接管回來 (Take Over) ,是另⼀組處理⼿續。

存儲模組撤離

若有資料庫維護的需要,⽽要移除存儲模組。假設須將最後⼀份存儲模組都移除,⽽不做該模組的平⾏代管。

存儲模組為全部的⽤⼾資料來源。假設⼀旦將全部可供⽤⼾驗證的⽤⼾資料全部載入暫存表,則可將存儲模組撤離。⽅式須在暫存模組與存儲模組⼆者預備⼀組 “load all user” 動作,並以此調閱批量⽤⼾資料,加入暫存表。完成後,即可撤離存儲模組。

另⼀情境為為了⽤⼩暫存模組適應⼤⽤⼾資料量,需有資料⽔平分割,以供多個暫存模組分段預

載。

以 Erlang/OTP 函數式程式設計實現 DDD 的一項設計案例。
Avatar of the user.
Please login to comment.

Published: Nov 30th 2022
20
2
0

Tools

erlang
Erlang

Domain-driven Design

Share