上一篇 閒談軟體設計:Web 框架的選擇提到,離開 Spring framework 後,需要有安全檢查與資料庫交易管理相關的配套,現在就來聊聊與安全檢查相關的議題,不過這次只聊怎麼「聲明」每個請求需要的安全性。
Spring Security 的寫法
Spring framework 裡的 Spring Security 提供非常彈性 (複雜) 的設計,包含身分驗證及權限檢查,這邊就先不討論身分驗證,只看權限檢查,Spring Security 有非常多種設定的方法,簡單一點的像是直接針對路徑設定 (範例來自官網):
或是彈性一點,針對每個函式設定 (範例來自官網):除了 Role-based,也可以用 ACL (範例來自官網):
Javalin 的寫法
Spring Security 可以說是相當彈性,那 Javalin 呢?在官網上可以看到,能對每個 endpoint 設定對應的角色,雖然沒有 Spring Security 來的多樣性,但很多情況下是相當夠用的。
到這邊為止,不管是用 .requestMatchers("/api/user/**").hasRole("USER")
或是 @PreAuthorize(hasRole("ADMIN"))
,還是 app.get("/private", ctx -> ctx.result("Hello private"), Role.LOGGED_IN);
,都是在做一件事:「聲明」這些請求需要怎樣的安全性。至於,是否滿足這些聲明,通常是框架幫忙處理了,讓開發者只需要聲明即可。
角色設計
回到這次要開發的系統。首先,系統有 O、M 和 S 三個不同組織階層 (細節不方便透露太多),及三個後端 (backends for frontends),針對不同的服務提供不同的 API endpoints,三個服務的帳號是分開管理,因此,A 服務的登入資訊是無法呼叫 B 服務的 API。
三個服務都有不同的角色設計,其中最複雜的,是每個資源階層都可以有擁有者 (owner)、管理者 (manager)、一般職員(staff) 以及財務 (finance) 四種角色。職員和財務都是唯讀 (read-only) 的角色,差別是財務相關的資料只有財務 (以及擁有者和管理員) 看的到,擁有者比管理者多了角色管理的權限。最重要的是,上層角色的權限適用於下層的資源,也就是 M 的管理員,也是S 的管理員。
函式風格的聲明
針對這樣的資源階層和角色階層,要怎麼聲明,確實有點麻煩。雖然 Java 的 annotation 很適合用來宣告,但在 runtime 處理 annotation 老實說我覺得很麻煩,我比較喜歡用別人寫好的,自己寫這些處理就有點懶,而且也不太符合 Javalin 的風格。最後,我希望使用起來的感覺像這樣:
一開始的 post
,指這個 endpoint 要用 HTTP POST,/resources/{sId}
,是該 endpoint 的路徑,最後是這個 endpoint 的 handler,被一個 secured
的函式包裝過,這函式的原型像是這樣:
第一個參數是實際的 handler,第二個參數是聲明 (JavalinSecurityClaim
),secured
回傳一個 wrapper,該 wrapper 在呼叫實際的 handler 之前,先呼叫 claim.fulfill
,如果不滿足聲明會拋出例外,忽略實際的 handler。
該如何提供聲明?上面的例子,anySResponsible()
意思是指該使用者能對 S 負責,具體來說必須是 S 的管理者或擁有者,或是 M 的管理者或擁有者,又或是 O 的管理者或擁有者,感覺很複雜?實際上它只有一行:
這邊是我個人偏好的命名,讓程式碼可以像一般的句子,這一行由三個函式組成,is
、anyResponsible
和 of
。先看 anyResponsible
,它用 any
函式 將 anyManager
和 anyOwner
包起來,any
是一個用來組合聲明的函式,只要組合的聲明哩,有任一個聲明滿足,就算是滿足,因此 anyResponsible
代表只要 anyManager
或 anyOwner
任一個滿足,就算是滿足。
JavalinSecurityClaim
和SecurityClaim
之間不是繼承關係,SecurityClaim
用來描述與框架無關的資訊,JavalinSecurityClaim
則是與特定框架綁定,將SecurityClaim
的資訊包裝成能在特定框架中執行的物件。
有了 any
,很容易組出「必須是 S 的管理者或擁有者,或是 M 的管理者或擁有者,又或是 O 的管理者或擁有者」,anyManager
將 RoleScope
這個 enum 的值 (O、M 和 S) 拿出來,各別產生一個 ScopedRoleSecurityClaim
物件,然後再用 any
組合起來。ScopedRoleSecurityClaim
的責任很簡單,檢查該使用者有沒有對應階層的角色。anyOwner
也比照辦理。
除了
any
,還有all
,必須是組合的聲明全部滿足才算滿足,但目前實際使用上,還沒有用到 all 的情境。
該有什麼角色已經確定了,但要怎麼知道從哪個階層的資源開始檢查?這就由 of
來處理,of
產生一個 PathResourceResolver
物件,這物件從請求的 context 中取得路徑上的變數,以 anySResponsible()
為例,它會試圖取得 /resources/{sId}
中 {sId}
的值,作為資源的 ID。最後 is
只是把角色 (anySResponsible()
) 和資源 (of("s")
) 封裝成 JavalinSecurityClaim
物件。
小節
這次選擇用函式來做聲明,搭配一些輔助函式,可以組合出不同的聲明,像是 anyMResponsible
,就是單純的 is(anyResponsibile(), of("m"))
,重點是透過函式組合,能重複利用,減少重複的程式碼。到這邊,就完成了聲明的設計,但是怎麼檢查這些聲明是否滿足,則是另一個故事了。