上一回 閒談軟體設計:安全聲明聊到怎麼替每個 endpoint 加上安全聲明,它就像是在入口貼一張紙,上面寫著:只有某單位的某角色或上層單位的某角色能進入,也可能是簡單的閒雜人等禁入。現在有某個人來了,身上別個通行證,我們僅依靠通行證就讓這個人進入嗎?這就是這回要聊的身家調查。
資料採信原則
對後端開發者在處理 request 大概都會有這個原則:前端資料不可信。產品上線後,有壞人會亂搞。所以那些資料可以採信就是後端要仔細思考的。這也是為什麼光是怎麼辨識 session,有人說不要用 XXX,有人說要用 XXX,各有各的說法。
沒有要戰前後端,目前我一個人同時寫後端和前端,要不是有找到好夥伴一起加入,連 app 都要自己寫,對我來說,我在開發「產品」,不是後端或前端。
一般來說,在使用者登入 (完成驗證) 後,後端會簽署一張憑證,後面所有的操作都是透過這張憑證,有時間限制的就像是一張臨時通行證,沒時間限制的就是永久通行證。但後端怎麼知道這個憑證不是假造的?簽署時通常會留檢核碼,後端在看到通行證後,把通行證上的資料和只有後端知道的資料再核算一次檢核碼,若檢核碼相同,這個通行證是偽造的機率就很低了。
但也就只有這樣了,至於有這個通行證的 request 是不是由當初驗證的人所發出,就沒有百分百的方式可以辨別了。
這樣就放行?當然不是,通行證是有可能被註銷的,因此還是要去查一下這張通行證的狀態,若沒問題,這張就是有效的通行證,到這邊,我們可以開始檢查這個人能不能進入了。如果這個入口僅是貼著「閒雜人等禁入」,那有通行證的人已經可以進入了;但如果「只有某單位的某角色或上層單位的某角色能進入」,剩下要考慮的便是角色來源和組織層級。
角色來源。曾有一陣子,有把使用者角色編到通行證裡的做法,這樣的好處是可以直接讀取通行證,省去較耗時的資料庫讀取。但缺點也很明顯,一旦使用者的角色變了,要註銷已發出的通行證,到換發新通行證前,所有的請求都會失敗。因此,在這次的系統設計裡,憑證只有最簡單的資訊:誰、簽發時間、到期時間及簽發單位 (還有檢核碼)。使用者擁有的角色是每次請求來的時候向 IAM 子系統查詢。
組織層級。在 RESTful API 的設計上,曾在閒談軟體設計:休息時間聊過,不過那時候並沒有特別提到,resource identifier 要不要把資源層級納入?例如,這次的組織層級有O、M、S 三層,那要存取 M 時,路徑應該是 /s/{mId}
還是 m/{oId}/{mId}
?這次設計時我使用前者,因為 oId
可能會誤導,例如 /m/o123/m1234
,很直覺會以為 m1234
屬於 o123
,但真的是這樣嗎?有沒有可能用有 o123
主管權限的憑證,用這路徑存取不屬於 o123
的 m1234
呢?既然不能相信,那就乾脆不用帶上來,只帶最低限度有用的資料上來就好。
這次選擇前者,不僅僅是安全上的考慮,這次為了加速開發,把 O 和 M 之間還有一層 C 的組織先忽略了,如果選擇後者,之後把 C 加回來,那 API 是否要跟著變動?這就讓我想到,在原始論文中有提到,API 應該隱藏內部細節,讓路徑是穩定鮮少變動的。
Security Context
既然前端的資料不可信,那檢查安全聲明時,什麼資料是可信的?為此,定義了一個 SecurityContext
介面,規範在檢查安全聲明需要的資料:(1) 使用者擁有什麼角色和 (2) 請求的資源 (Resource
) 存不存在。
而這些資料由 SecurityContextResolver
的實作提供,負責從請求的 Context 中試圖解析出來,其中一個實作大概像這樣:
- 先用
this.subjectResolver.resolve(context)
找出是誰 - 接著用
this.resourceResolver.resolve(context)
找到請求的資源,還記得上回的PathResourceResolver.of("s")
嗎? - 然後用迴圈的方式,一路詢問該資源的上層資源是什麼?
最後把所有的資料整合在一起,也就說身家調查,不只調查使用者的角色,也調查了請求的資源的祖宗十八代。
Java 的
Optional
雖然有ifPresent(consumer)
的用法,卻沒有 guard let return 的形式有點不方便,只能用orElseThrow()
來提前離開,但不是每個地方都適合拋出例外,有趣的是,有時候我不用ifPresent(consumer)
的原因是 consumer 不允許拋出例外…
有了這些資訊,ScopedRoleSecurityClaim
的實作就很單純了:
- 先用請求的資源類型,來找資源
- 接著用「找到的資源」和「聲明的內容」來檢查是否有對應的角色
拿上回提到的 anySResponsible()
,由三個 ScopedRoleSecurityClaim
組成,假設按順序是 O 的主管或擁有者、M 的主管或擁有者和 S 的主管或擁有者,這三個實體分別顯查 O、M 和 S 資源存不存在,假設存在,再問是不是有對應的角色,只要其中一個滿足即可。
小結
整個身家調查的過程中,只有最一開始 PathResourceResolver
提供的 sId
來自前端,其餘資訊都是從資料庫取得,即便是 sId
也會檢查對應的資源存不存在。搭配彈性的安全聲明,就可以完成不同形式的安全檢查了。