談談用戶許可權系統
登錄這事之於一個需要識別用戶身份的產品,就彷彿cs101之於computer science。感謝各種語言里各種優秀的登錄模塊(比如nodejs的passport),絕大多數產品,把它們拿來配置一下,閉著眼睛,花點功夫,就完成了一個從用戶註冊到登錄一條龍的服務。很好很強大,不需要較真,也沒人較真。
可登錄還真是一件即便你半天就搞定還是需要好好較一下真的問題。本文回歸本源,談談登錄中那些極其重要又被人忽視的思想。
首先需要回答的一個問題是:要求用戶登錄的目的何在?
這個問題的答案是不言而喻的。伺服器上的資源並非人人可以訪問和操作,我們需要識別用戶身份,從而了解他可以訪問哪些資源,完成哪些操作。這裡面隱含著幾個重要的概念:
資源(resource)
操作(operation)
身份,或者角色(role)
先看「資源」。如果你設計一個聊天系統,那麼,為聊天而建的群組(channel),在群組中大家暢所欲言發表的信息(message)就是資源的概念。這個很好理解。
「操作」是附著在「資源」上的用戶行為。在某個資源上,最基本的操作是:讀(read),寫(write),執行(execute)。那麼,讀/寫/執行究竟怎麼理解呢?聊天系統列出(list)當前所有可見的群組,或者顯示(show)某個群組下的某條聊天記錄,這便是讀操作;某個用戶創建(create)一個群組,修改(update)群組信息,發表(create)聊天記錄,撤銷(delete)一條聊天記錄,這些都是寫操作的範疇。至於在聊天記錄裡面全文搜索(search),存檔(archive)舊的聊天記錄,可以被視作執行。
操作示例read
列出所有群組/顯示某條聊天記錄,或者說 list/show
write
創建群組/修改群組信息/發表聊天記錄/撤銷聊天記錄,或者說 create/update/delete
execute
全文檢索/存檔,或者說 search/archive
讀/寫/執行是最基本的操作,而list/show/create/update/delete/search/archive是具體的操作。
「角色」是一個用戶屬性,定義用戶對資源的訪問許可權。上述的聊天系統可能的角色有:所有用戶(all users),匿名用戶(anonymous users),已登錄用戶(authenticated users),群主(更廣義一些說,resource owners)以及管理員(administrators)。這五個角色是一個系統最基本的角色,在此基礎上可以衍生出來一些特定的角色,比如群成員。
對於一個「角色」來說,其訪問許可權可以通過訪問列表(ACL,access list)來定義。一般而言:
所有用戶不能進行任何操作
匿名用戶可以進行讀操作
已登錄用戶可以進行創建資源(特定的寫操作)
資源擁有者可以對自己創建的資源進行任何寫操作(修改/刪除)
管理員可以對任何資源進行寫操作
web應用的訪問列表的功能可以類比網路中的防火牆的功能:
對於我們舉的聊天系統的例子,具體的訪問列表可能是這個樣子:
所有用戶不能進行任何操作
匿名用戶只能執行登錄/註冊操作
已登錄用戶可以創建群組(寫)
已登錄用戶可以讀取群組列表(讀)
已登錄用戶可以加入群組(執行)
群成員可以發信息(寫)
群成員可以刪除自己最後發出的信息(寫)
群主可以修改群組信息(寫)
群主可以批准加入請求(執行)
群主可以把不良分子驅逐出群(執行)
…(管理員就不列了)
把這些訪問列表以yaml的形式定義,大概是這個樣子:
當系統里每個角色都有了定義清晰的訪問列表後,一個用戶的登錄行為實際上就是動態遷移角色的行為。比如說,登錄前小明的角色是 [所有用戶, 匿名用戶],登陸後他的角色轉化為 [所有用戶, 已登錄用戶],當他創建群組A後,並進入群組A後,他的角色轉化為 [所有用戶, 已登錄用戶, A群成員, A群群主],當他加入群組B,開始聊天時,他的角色又轉化為 [所有用戶, 已登錄用戶, B群成員]。無論小明訪問系統的哪個部分,我們都能找到他對應的角色,進而算出他擁有的許可權的集合(所有角色的訪問列表的並集)。
有同學可能會認為「所有用戶」這個角色,以及「所有用戶不能進行任何操作」這個訪問列表有些多餘,其實,這正是系統設計嚴密性的一種體現。就如一個防火牆,其默認的策略是「從任意源到任何目的地的網路數據都丟棄」,或者一段switch case,最後總需要有一個default是同一個道理。一個用戶在極端的情況下可能沒有附加任何角色,或者請求的操作並未找到對應的訪問列表,那麼能唯一匹配的訪問列表就是「所有用戶不能進行任何操作」(all, *, *, DENY),所以不允許他做任何事情,在邏輯上是嚴密的。
定義好了資源,對資源允許的操作,用戶可以附加的角色,以及角色擁有的訪問列表這些最基本的內容之後,整個用戶許可權系統就清晰多了。你再也不必用散落在各處的代碼苦心孤詣地從上下文里扒拉出來這個用戶究竟允不允許做當前的操作,而是通過在請求的入口處設立一道閘門(middleware),擋掉不合法的請求,只允許合法的請求通過這道閘門,閘門的設計很簡單:
guard(resource, operation, role_list)n
其中,resource和operation必然在請求中包含,比如一個http請求:https://api.chat.wtf/channels/wtf/actions/send/, wtf(組名)就是resource,send就是operation。而role_list可以在user session里找到。
這大大簡化了許可權處理,而guard本身,實際上就是一個acl lookup engine。你可以找現有的解決方案,也可以把所有定義好的訪問列表塞到一個hash table里,放在redis里進行快速查詢,當然,如果你會一門趁手的函數式編程語言,比如elixir,可以直接做pattern matching:
def do_guard("channel", "$owner", "$execute") don "ALLOW"nendnn...nndef do_guard(_, _, "$all") don "DENY"nendn
對於那些允許管理員在後台修改訪問列表的系統,我們還可以使用使用elixir的macro功能,在每次後台修改完成後,觸發重新生成acl lookup engine,並利用erlang VM的特性,hot code reload到系統中。
如果您覺得這篇文章不錯,請點贊。多謝!
歡迎訂閱公眾號『程序人生』(搜索微信號 programmer_life)。每篇文章都力求原汁原味,北京時間中午12點左右,美西時間下午8點左右與您相會。
推薦閱讀:
※世界盃情緣
※奇博士的管理課 - 激勵
※且寫且珍惜
※[讀者留言] 程序人生 - 且行且珍惜
TAG:迷思 |