iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
Software Development

30天打造一套企業PLM系列 第 7

Day 7:LDAP 整合與 RBAC 設計

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20260824/20161290KXwtpBxlZJ.jpg

系列:30 天打造企業級 PLM|面向:後端

問題場景

企業系統的使用者體驗,第一條就是不能讓員工多記一組密碼。公司已經有 AD(Active Directory),PLM 就該用 AD 帳號登入;登入之後這個人能看哪些選單、能按哪些鍵,則是 RBAC 授權鏈的工作。今天講認證(LDAP)與授權(RBAC)怎麼在 Mini-PLM 落地,外加兩個帳號管理的經典坑。

https://ithelp.ithome.com.tw/upload/images/20260824/20161290V3Y4OeiBgG.png

商業邏輯設計

  • RBAC 跟著組織圖走:Role 對應職能(工程師、審核者、管理員),UserGroup 對應組織或任務編組(工程部、NPI 小組)。人事異動時改的是「人與群組的關係」,不是幾百條個人權限
  • 群組 owner 制:每個 UserGroup 有 owner(可多位),群組成員誰進誰出由 owner 自己維護。權限維護下放給業務主管,IT 不當人肉審批機
  • 角色選單可自訂:每個 Role 可以掛上自己的 Menu 集合,同一個帳號因取得的角色不同,就能呈現不同的選單入口;但選單可見性不取代後端功能授權
  • 帳號生命週期:離職是停用或軟刪除,不做實體刪除,歷史簽核紀錄上的名字必須永遠查得到

技術選型與取捨

共同基礎:Mini-PLM 與 Oracle Agile 都採用 Role-based 授權

先釐清一個容易寫錯的比較:Mini-PLM 並不是另創一套與 Oracle Agile 完全不同的權限模型,兩者都以 User/User Group → Role → Privilege 作為核心。權限架構:

https://ithelp.ithome.com.tw/upload/images/20260824/20161290gpm77D913f.png

圖解閱讀:User 或 User Group 先取得 Role,Role 再聚合 Privilege Mask;Privilege 定義能做什麼,Criteria 定義這項權限適用哪些資料與條件。

也就是說,Oracle Agile 的 Privilege + Criteria 會組成 privilege mask,再掛到 Role;EJB 或其他後端攔截層是執行授權的技術方式,不是單純的 RBAC 模型。Oracle 官方文件也明確以「Privilege 搭配 reusable Criteria,再由 Role 集合管理」說明這條授權鏈:Privileges and Privilege Masks。這也是為什麼排查「看不到某筆料號」時,必須沿著 User/Group、Role、Privilege Mask、Criteria 一路追查,而不能只看登入是否成功。

Mini-PLM 沿用相同的角色授權思想,但把應用程式常見的授權面拆成幾個可直接維護的關聯:

比較面向 Oracle Agile PLM Mini-PLM
共同骨架 User/User Group 取得 Role,Role 聚合權限 Account 可直接取得 Role,也可經 UserGroup 取得 Role
功能與資料特權 Privilege 搭配 reusable Criteria 形成 privilege mask MP_ROLE_PRIVILEGEMP_PRIVILEGE 掛到 Role;特權可帶目標物件、欄位、資料表與可執行的 ConfigCriteriaNode
導覽呈現 權限模型的重點是功能與資料存取 額外以 MP_ROLE_FOLDERMP_MENU 掛到 Role,組出每個角色的可見選單
組織維護 使用者與 User Group 的角色指派 UserGroup 另有 owner 與成員關係,可將角色與選單配置下放給業務維護

因此,兩者的主要差異不是「Agile 有 RBAC、Mini-PLM 沒有」,而是 Privilege 與 UI Menu 明確拆開,並增加「角色可自訂選單」這個應用層能力。這樣可以讓工程師與管理員擁有不同的入口,即使兩者仍共用某些後端權限規則。

圖解閱讀提示:左側呈現的是 Agile 在 privilege、criteria 與後端攔截層疊加後的排查複雜度,不代表 Agile 沒有 RBAC;兩邊共同的授權骨架仍是「使用者/群組 → 角色 → 權限」。右側的 Menu 是 Mini-PLM 額外提供的角色級 UI 投影。

https://ithelp.ithome.com.tw/upload/images/20260824/20161290cBGUlFS9ax.png

Mini-PLM 的關鍵差異:Criteria 是可執行的動態權限範圍

前面的比較表如果只寫「Privilege 可帶 Criteria」,仍然不足以表達 Mini-PLM 的特色。Mini-PLM 的 Criteria 不是掛在權限旁邊、只供管理員閱讀的靜態備註,而是可以和動態查詢結合的條件樹(ConfigCriteriaNode)。它會在每次授權判斷時,拿目前資料重新比對,決定這個 Role 的 Privilege 是否適用於這筆資料。

https://ithelp.ithome.com.tw/upload/images/20260824/201612901gluHw3n3O.png

圖解閱讀:CriteriaNode 不是單純備註,而是會被執行的條件樹;系統將它套入目前資料後,依比對結果決定是否套用該 Role 的資料/欄位權限。

例如,同一個 MODIFY Privilege 可以只允許使用者修改「符合某個部門、狀態或其他業務條件」的表單;管理員調整 Criteria 的條件後,權限範圍就能隨資料與設定變化,不必為每一種範圍另寫一條硬編碼的 Java 規則。實際執行上,AuthorizationService 會透過 QueryService.matchFormByCriteria() 判斷單筆資料;列表或搜尋情境則使用 matchFormsByCriteria() 批次比對,並由 resolveReadableFieldsBatch() 避免逐筆資料乘上每個 Criteria 的查詢成本。

這讓 Mini-PLM 的授權可以拆成三個層次理解:

  1. Role:User 或 UserGroup 是否具有某個 Role。
  2. Privilege:這個 Role 能不能執行 READ/MODIFY,以及能操作哪些欄位。
  3. Criteria:這筆資料是否落在可操作的動態範圍內。

因此,Criteria 才是把「角色有某項權限」進一步收斂成「角色對哪些資料有某項權限」的關鍵。選單只負責呈現入口,Criteria 則參與真正的資料級授權;更新授權設定後仍須配合既有的快取清除或更新事件,避免舊的授權結果暫時留在快取中。

複雜權限查詢:先用快速 deterministic 路徑,再接 AI

當權限關係同時包含直掛 Role、UserGroup、Role、Privilege 與 Criteria 時,單靠人工逐張資料表追查並不適合。Mini-PLM 另外提供管理員使用的權限診斷 API:

POST /api/v1/admin/diagnosis/permission

這條路徑將複雜查詢固定成可追蹤的授權鏈:ACCOUNT_EXISTSROLES_RESOLVED → 選單或 Privilege 綁定 → CACHE_HINT,回傳 ALLOWEDBLOCKED,並把每個步驟標示為 PASS、FAIL、WARN 或 SKIPPED。也就是說,管理員可以先快速回答「這個帳號是否透過某個 Role 具備某個選單或 Privilege」,不用把登入、群組、角色與權限關聯人工拼湊一次。

目前這是一個 deterministic 的結構化查詢服務,AI 尚未直接代替授權判斷。未來可以讓 AI 將「請查王小明是否有修改 P1 表單的權限」解析成帳號、目標類型、Privilege 與表單類型,再呼叫這條服務;如果問題還包含特定資料範圍,則再把同一套 Criteria 動態比對納入診斷。最後由 deterministic 結果作為唯一依據,AI 只負責自然語言解析與白話說明,才能保留可稽核、可重現的權限答案。

認證方式可切換:ldap 或 db

認證方法由設定檔決定,同一套程式支援「AD 驗證」與「DB 驗證」兩種模式(AuthService.authenticate() 實碼節錄):

String authenticateMethod = ymlData.getAuthenticateMethod();

if ("ldap".equalsIgnoreCase(authenticateMethod)) {
    log.info("login by ldap");
    // === LDAP 端:僅負責密碼驗證;
    //     拋 AuthenticationException 代表 LDAP 端失敗 ===
    ...
}
// 首次登入自動同步 AD 屬性(如 email)
if ("ldap".equalsIgnoreCase(authenticateMethod)
        && ldapUser != null && persistentUser.getEmail() == null) {
    persistentUser.setEmail(ldapUser.getMail());
}

職責劃分很清楚:LDAP 只管密碼對不對,本地資料庫管這個人是誰、能做什麼。帳號主檔(mp_account)永遠在本地,權限、主管鏈、偏好語系這些 AD 沒有的欄位都掛在上面;AD 有的屬性(email)首登時同步進來。這樣即使哪天換掉 LDAP,授權體系一行都不用改。

https://ithelp.ithome.com.tw/upload/images/20260824/20161290dL5AJHK3Tt.png

RBAC 授權鏈的資料結構

實際 schema 把「角色取得」「端點授權」「功能特權」與「選單投影」分開:

mp_account ──< mp_user_role >── mp_role          ← 人直接掛角色
mp_account ──< mp_user_usergroup >── mp_usergroup ← 人屬於群組
mp_usergroup ──< mp_usergroup_role >── mp_role    ← 群組掛角色(批次授權的主力)
mp_usergroup ──< mp_usergroup_owner >── mp_account ← 群組負責人(M:N,可多位 owner)
mp_role ──< mp_role_permission >── mp_permission  ← HTTP method + URI 存取控制
mp_role ──< mp_role_privilege >── mp_privilege    ← 功能/資料特權(Day 6 的 Privilege)
mp_role ──< mp_role_folder >── mp_menu            ← 角色可自訂可見選單

登入後的授權計算分成三條線:

  1. buildAuthorities() 將使用者「直掛角色 ∪ 群組角色」收斂成 Spring Security authority,並以 LinkedHashSet 去重。
  2. getUserMenus() 依同一批角色查詢 mp_role_folder,只取 MenuEnum.Folder 並組成選單樹;因此不同 Role 可以呈現不同的入口。
  3. getUserPrivileges() 查詢 mp_role_privilege,供功能/資料層的後端檢查使用。

所以更精確的說法是:選單是授權結果的 UI 投影,不等於完整的後端權限。前端可以隱藏沒有入口的功能,但真正的 API 與資料操作仍必須由 Privilege 在後端再次判斷;其中 Criteria 不是靜態說明,而是可執行的動態查詢條件,會進一步決定這筆資料是否落在可讀或可修改範圍。Day 17 前端會接手選單投影與按鈕顯示這條線。

https://ithelp.ithome.com.tw/upload/images/20260824/201612902XGzPEYnfb.png

踩坑記錄(一):鎖定功能全部失效的一行 bug

帳號防暴力破解機制:登入失敗計數、達門檻自動鎖定、管理員也能手動鎖。功能上線後一測,怎麼鎖都鎖不住。程式邏輯反覆看都是對的,最後發現問題在資料流:loadUserByUsername 用 JPQL projection 查帳號核心欄位,而 projection 漏了鎖定相關的三個欄位。現在的程式碼裡留著這段血淚註解:

// 登入閘門 isAccountNonLocked() 需要 accountLocked / lockUntil;
// registerLoginFailure 需要 failedLoginCount。未載入會讓 principal
// 這三欄恆為 null → 手動鎖與自動鎖在真實登入完全失效。
authAccount.setAccountLocked(core.getAccountLocked());
authAccount.setLockUntil(core.getLockUntil());
authAccount.setFailedLoginCount(core.getFailedLoginCount());

教訓有兩層。其一,功能寫對了不代表資料流通了:鎖定判斷讀的是 principal 上的欄位,principal 是 projection 組出來的,中間斷一節就全盤失效。其二,這類 bug 單元測試抓不到(測試裡 entity 是完整的),只有真實登入路徑的整合測試會現形。另一個設計決策是 ADMIN 帳號豁免自動鎖:管理員被鎖死就沒人能解鎖了,韌性優先於一致性。

https://ithelp.ithome.com.tw/upload/images/20260824/201612902woNgtiQye.png

踩坑記錄(二):軟刪除的列還佔著唯一鍵

帳號刪除用 JPA 軟刪除(@SQLDelete 改 UPDATE deleted 旗標 + @Where 過濾),歷史紀錄的關聯得以保全。但唯一約束不會理你的旗標:

使用者刪掉帳號 A → 畫面上消失了 → 重建同名帳號 A → 409 Conflict。因為那筆 deleted=true 的列還躺在資料表裡,佔著 username 的唯一鍵。

使用者的認知是「我刪掉了」,資料庫的事實是「它還在,只是看不見」。解法看資料性質:帳號這種要保歷史的,唯一鍵改成複合(含 deleted 旗標),或重建時改走「復活」既有列;拋棄式資料(如暫存設定)乾脆改硬刪除,bulk JPQL DELETE 繞過 @SQLDelete。軟刪除加唯一約束天生互斥,設計時就要選邊,不要等 409 教你。

小結

今天的主軸是認證與授權分家:LDAP 只驗密碼,身分與權限留在本地,換 LDAP 不動授權;授權鏈跟著組織圖對齊,群組做批次授權、owner 下放維護權。至於兩個坑(projection 斷流、軟刪除佔鍵),共通點是邏輯都對、資料流斷了,這種 bug 最會躲測試。

明日 Day 8:動態表單引擎,Day 4 的 metadata 如何在前後端「活」起來,以及「沒權限的欄位該傳什麼」。


上一篇
Day 6:JWT Stateless 認證與 Spring Security
系列文
30天打造一套企業PLM7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
jeff377
iT邦新手 3 級 ‧ 2026-08-24 10:17:43

僅提供參考,比較完整的權限控管可以分 Action、Row、Column 三個維度
.Action : 能不能做這件事 (View、Update、Delete 等動作)
.Row : 能不能看或編修這筆資料
.Column : 能不能看或編修這個欄位

由權限去控管選單,應該算是 Action 維度,但需不需要做到 Row 及 Column 可能要視公司規模、產業特性、實際需求而定。

謝謝補充, 不過這裡的選單跟權限是分開處理的
選單會根據部門決定選單呈現, 比較像Portal的設計

權限的規畫還是跟Oracle Agile一樣
Role-Action-Criteria-Column
最複雜的是Criteria, 可透過設定查詢條件動態建立

稍微調整內容, 特別把criteria的部分加強說明
後面內容會再詳細說明動態查詢的實作方式

我要留言

立即登入留言