
做AI SaaS平臺這兩年我最大的體會就是權限體系這種東西看著不起眼真到了模型一多、用戶一多、租戶一多的時候它會變成整個系統里最容易出事故、也最難改的一塊。之前我們平臺剛起步時權限就兩個角色管理員和普通用戶菜單里藏一下按鈕就完事了。后來接入的AI能力越來越重有的租戶要調GPT-4o有的只需要輕量摘要還有人要按調用次數計費這時候才發現權限不是“誰能登錄后臺”的問題而是“誰能調用什么模型、能用多少額度、能看哪些數據”的立體問題。這篇文章就圍繞如何用RBAC基于角色的訪問控制實戰落地一套AI SaaS平臺的權限體系展開。我盡量講清楚整個設計鏈路從數據表怎么建、權限點怎么命名、到模型調用怎么鑒權、配額怎么扣、審計日志怎么做再到實際開發中哪些坑最容易踩。適合正在做SaaS平臺、AI應用后端、或者準備重構權限模塊的團隊參考也適合剛接觸權限設計的后端工程師至少能幫你少走幾條彎路。1. 為什么AI SaaS平臺的權限體系不能只靠“登錄 角色”1.1 傳統后臺權限和AI SaaS平臺的差異傳統的后臺權限管理通常解決的是“誰能進入哪個頁面、誰能點哪個按鈕”的問題。用戶登錄后拿到一個角色角色關聯一批菜單和操作權限后端接口再做一下攔截基本就完事了。這種模型在純信息管理類系統里非常成熟也是RBAC最經典的應用場景。但放到AI SaaS平臺上事情變復雜了因為你要控制的資源不只是頁面和按鈕還包括AI模型本身的調用、Token額度、并發數、數據隔離范圍、甚至不同模型的不同版本。舉個例子我們平臺上有一個客戶成功團隊和一個開發者團隊前者只能白屏操作調用摘要模型后者需要直接調API并且可以配置prompt模板。如果只按“管理員/普通用戶”分要么開發者拿到過多權限要么客戶成功團隊什么都干不了。更麻煩的是AI模型調用是有成本的和合規要求的你不光要管“能不能調”還要管“能調幾次”“能調哪個模型”“調完之后日志留沒留下”這些已經不是傳統RBAC能直接覆蓋的范圍了。所以說AI SaaS平臺的權限體系本質上是在傳統RBAC之上疊加了資源權限、配額權限、租戶數據權限和審計要求。RBAC依然是最穩的底座但必須在底座上做擴展。1.2 RBAC模型怎么選從RBAC0到RBAC2再到RBACABAC很多同學知道RBAC但不一定清楚RBAC本身也分幾個級別。做設計之前先把模型選對能省掉后面大量返工。RBAC0用戶直接關聯權限沒有角色這層抽象。小項目能用但一旦權限一多用戶和權限之間直接爆炸基本不推薦。RBAC1引入角色繼承角色可以嵌套。比如“運營”繼承“基礎用戶”的所有權限再額外加一些導出權限。這種模型很適合權限有天然層級關系的平臺。RBAC2引入角色約束包括角色互斥用戶不能同時擁有兩個互斥角色、角色基數每個角色的人數上限、先決角色要擁有某角色必須先有另一個角色。這在金融、企業服務里很常見。RBAC3RBAC1RBAC2既支持繼承又帶約束能力最全但復雜度也最高。我實際做AI SaaS平臺時建議主體用RBAC1加部分RBAC2的約束比如“模型管理員”和“財務管理員”做成互斥角色避免權限過度集中。至于那種需要根據上下文動態判斷的場景比如“只允許在工作時間調用敏感模型”或者“同一個角色在不同租戶下看到不同數據”純RBAC處理不了我會額外引入ABAC策略來補充而不是把RBAC硬拗成萬能模型。這里說一個很實用的判斷標準如果權限判斷的依據是“用戶身份是什么”用RBAC如果依據是“請求時的環境、資源屬性、上下文”用ABAC。AI SaaS平臺里前者解決90%的問題后者解決最后那些動態規則。1.3 純RBAC和ABAC結合的落地思路純ABAC的問題在于規則太難維護。你讓業務去寫幾十條策略表達式他們根本看不懂。但RBAC的好處是直觀——給某個角色勾選權限點產品經理和業務都看得明白。所以我的落地思路是先把系統里所有可控制的動作抽象成權限點關聯到角色上這部分全部走RBAC。在此基礎上再留一個規則引擎擴展點專門承接“限時可用”“限資源可用”這類臨時策略。比如某天要上線一個灰度模型只允許白名單租戶調用我就在規則引擎里加一條“當租戶ID在whiteList時允許訪問model:invoke:new-model”而底層角色權限完全不用動。這樣既保住了RBAC的簡單性又不至于在動態需求面前束手無策。后面章節里的表結構設計也完全兼容這種“RBAC為主、ABAC為補充”的方案。2. 權限模型設計核心表結構與關鍵字段解析2.1 五張核心表的SQL設計權限系統的地基是表結構尤其要注意不要設計成“用戶表里塞一個role字段”這種省事方案。后期你要查“哪些用戶擁有某個權限”“某個角色都關聯了什么權限”的時候一張冗余字段表會讓你想罵人。我推薦至少五張表用戶表、角色表、權限表、用戶-角色關聯表、角色-權限關聯表。字段上不一定要完全照抄我的設計但以下這些核心點值得保留。CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(64) NOT NULL UNIQUE, password_hash VARCHAR(128) NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1啟用 0禁用, tenant_id BIGINT NOT NULL COMMENT 所屬租戶, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); CREATE TABLE sys_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, role_code VARCHAR(64) NOT NULL UNIQUE COMMENT 角色編碼如 MODEL_ADMIN, role_name VARCHAR(64) NOT NULL, parent_id BIGINT DEFAULT NULL COMMENT 父角色ID支持角色繼承, status TINYINT NOT NULL DEFAULT 1, tenant_id BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE sys_permission ( id BIGINT PRIMARY KEY AUTO_INCREMENT, perm_code VARCHAR(128) NOT NULL UNIQUE COMMENT 權限點編碼如 model:invoke:gpt-4o, perm_name VARCHAR(128) NOT NULL, category VARCHAR(64) DEFAULT NULL COMMENT 分組如 MODEL_ACCESS / QUOTA / AUDIT, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE sys_user_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, tenant_id BIGINT NOT NULL, UNIQUE KEY uk_user_role (user_id, role_id) ); CREATE TABLE sys_role_permission ( id BIGINT PRIMARY KEY AUTO_INCREMENT, role_id BIGINT NOT NULL, permission_id BIGINT NOT NULL, UNIQUE KEY uk_role_perm (role_id, permission_id) );這里有一個重要細節所有業務表都加了tenant_id。原因很簡單SaaS平臺天然多租戶如果權限表本身不按租戶隔離后面做數據權限會非常痛苦。雖然sys_permission可以做成全局共享但角色和用戶的關系必須綁定租戶。比如A租戶的“模型管理員”和B租戶的“模型管理員”同名但數據完全不能互通。2.2 中間表為什么要存在而不是用戶表直接帶角色ID寫單表快但你會立即撞上“一人多角色”這種需求。產品經理大概率會說“這個用戶既是模型管理員又是審計員他的權限是兩者疊加。”如果你在用戶表里只放一個role_id這需求直接做不了。用中間表可以支持多對多多角色時權限取并集這個其實很簡單但要注意權限疊加可能導致越權。比如一個角色有“導出用戶數據”權限另一個角色剛好有“查看所有租戶”的數據范圍權限兩者疊加就可能把全平臺數據導出。遇到這種情況就要在RBAC2里加約束或者靠權限審批流程來控制后面我會在“常見問題”里展開講。2.3 把權限點設計成“資源標識”統一命名與路由映射權限點編碼是整個體系中容易被低估的設計。很多團隊直接寫“用戶管理”“模型管理”這種中文命名短期能用但接口一多、模型一多維護起來就是災難。我建議用三段式命名模塊:動作:資源。模塊通常對應系統域動作是動詞資源是被操作的對象。例如model:invoke:gpt-4o 表示允許調用GPT-4o模型model:invoke:gpt-4o-mini 表示允許調用輕量模型quota:update:user 表示允許調整用戶配額audit:view:log 表示允許查看審計日志prompt:write:template 表示允許編寫提示詞模板后端做校驗時直接在接口代碼里聲明需要的權限點例如“調用模型接口需要model:invoke:gpt-4o”。前端拉取權限點列表后控制菜單顯隱、按鈕狀態。這樣前后端用同一套權限點編碼理解成本低排查問題也方便。權限點不建議用數據庫自增ID直接傳給前端因為ID在不同環境可能不一致容易出現測試環境正常、生產環境錯亂的靈異問題。用字符串編碼做唯一標識天然可讀、可遷移環境切換幾乎沒有成本。2.4 數據權限與租戶隔離容易被忽略的維度角色權限解決的是“能不能操作”數據權限解決的是“能操作哪些數據”。在AI SaaS平臺里這兩個維度必須分開設計。舉個例子兩個租戶都開通了某個模型服務如果接口鑒權只校驗角色不校驗租戶ID用戶用A租戶的token調用接口傳入的卻是B租戶的模型配置ID就可能讀取到其他租戶的數據。這種越權在AI應用里非常隱蔽。我通常這么處理先按RBAC判斷“能不能做”再按數據權限過濾“能做哪些”。數據權限分三級就夠了全部數據、本部門/本租戶數據、僅本人數據。具體的隔離方式SaaS平臺一般有三種選擇隔離方式說明適合場景獨立數據庫每個租戶一個庫隔離最徹底大型客戶、合規要求高獨立Schema同庫不同Schema中型SaaS共享表 tenant_id所有租戶共用同一張表行級隔離中小型SaaS起步期我建議絕大多數AI SaaS平臺起步階段用“共享表 tenant_id”成本最低也最靈活。關鍵是要有一個全局的TenantContext比如基于ThreadLocal或請求上下文把當前租戶ID塞進去然后所有的數據訪問層都強制帶上tenant_id條件不允許靠開發人員自覺。3. 結合AI能力擴展模型調用權限、配額與審計3.1 把AI模型當作受控資源模型網關設計AI SaaS平臺和普通SaaS最不一樣的地方就是底層API全是模型調用。模型不是免費資源也不是隨便哪個角色都能碰所以要把模型當成一類“資源”來管理。我的做法是加一層模型網關所有模型調用統一走網關不在業務代碼里直接拼OpenAI或者其他廠商的SDK調用。模型網關的核心職責有三個鑒權、配額校驗、轉發。所謂的“模型權限”本質上就是網關里的一組白名單配置判斷當前用戶所在的角色是否擁有目標模型的調用權限。{ model: gpt-4o, allowed_roles: [ROLE_MODEL_ADMIN, ROLE_ENTERPRISE_USER], allowed_plans: [enterprise, pro], rate_limit: { rpm: 60, tpm: 100000 }, quota_cost: 20 }上面這個配置的意思是只有模型管理員和企業用戶角色能調用gpt-4o且套餐必須是enterprise或pro每分鐘最多60次請求每次調用消耗20個credits。網關拿到請求后先解析用戶角色再和配置比對不滿足直接返回403滿足就進入配額扣減流程。這個設計的優點很明顯新增一個模型只需在網關里加配置不需要改一堆業務代碼。比如平臺接入了新的圖像生成模型要開放給運營角色只需要在網關配置里把運營角色加進白名單再設置好配額成本前后端代碼零改動。3.2 Credits配額設計權限不只是“能不能用”還要管“用多少”AI模型按調用量計費所以權限系統必須對接配額系統。很多團隊一開始不做配額結果月底賬單出來老板傻眼。配額本質上是一張“余額表”記錄每個用戶或角色在某個周期內可用多少次模型調用。Credits在AI產品里通常指計量配額。我的方案是用戶賬戶表里存總credits余額模型配置表里存每次調用消耗多少credits每次調用前做余額預校驗不足直接拒絕調用成功后異步扣費失敗則返還這里有一個非常容易出問題的點并發扣費。用戶連續點了幾次“生成圖片”如果業務代碼是“先查余額再判斷再扣減”并發請求可能同時通過校驗導致余額變成負數。解決方式是用Redis的Lua腳本做原子扣減或者用數據庫樂觀鎖。我習慣在網關里直接做預扣這樣效率高一點-- 簡單演示原子扣減 credits local current tonumber(redis.call(GET, KEYS[1]) or 0) local cost tonumber(ARGV[1]) if current cost then return -1 else redis.call(DECRBY, KEYS[1], cost) return current - cost end扣減完成后再去調模型API萬一模型調用失敗再執行“返還”邏輯給用戶的credits加回去。這個“預扣-回調-返還”的流程比調用成功后再扣費可靠得多因為模型調用是網絡IO超時、報錯太常見了調用成功后再扣費很容易漏扣。3.3 審計與內容安全AI場景更容易出問題傳統后臺的審計日志記“誰刪了什么數據”就夠了AI SaaS平臺的審計更復雜。你得知道誰在什么時間調用了哪個模型、傳了什么輸入、模型返回了什么摘要。一方面是為了排查濫用另一方面是為了滿足合規要求。我的建議是至少維護一張審計表CREATE TABLE ai_audit_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, tenant_id BIGINT NOT NULL, model_code VARCHAR(64) NOT NULL, action VARCHAR(32) NOT NULL COMMENT invoke / retry / fallback, prompt_hash VARCHAR(64) DEFAULT NULL COMMENT 輸入內容哈希用于溯源, prompt_text TEXT DEFAULT NULL COMMENT 輸入內容需要脫敏, output_text TEXT DEFAULT NULL COMMENT 輸出內容摘要, cost_credits INT NOT NULL DEFAULT 0, status TINYINT NOT NULL COMMENT 0失敗 1成功, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user_time (user_id, created_at), KEY idx_tenant_time (tenant_id, created_at) );注意prompt_text和output_text必須脫敏不能直接原樣存尤其是涉及用戶隱私或企業機密的內容。實際操作里我會先跑一遍脫敏規則把手機號、郵箱、身份證號替換掉再落庫。內容安全層面AI生成內容需要接入合規檢測檢測不通過要攔截返回。這個可以放在模型網關里做模型調用前先對輸入做審查輸出回來后對內容做審查兩邊都不放過。我們之前就踩過輸出側漏審的坑用戶輸入本身合規但模型生成的文案里帶了違規內容好在有輸出檢測兜底否則問題就大了。4. 鑒權實現從登錄態到權限校驗的完整鏈路4.1 登錄態與JWT權限信息放哪里權限設計得再好最終都要落到鑒權執行鏈路上。最常見的方案是用JWT做登錄態用戶在登錄后拿到一個token后續請求帶token訪問。問題來了JWT里到底放不放權限信息我見過有人把用戶所有權限點塞進JWT省得每次查庫。但這樣做有兩個坑一是JWT是無狀態的權限變更后舊的token依然有效導致權限更新不及時二是JWT體積膨脹每次請求都帶著一大串權限列表浪費帶寬。我的做法是JWT里只放用戶ID、租戶ID、角色編碼列表這些輕量信息不放全量權限點。每次請求進來后用用戶ID去緩存里拿權限點集合。權限變更時通過版本號機制讓緩存失效這樣既保證了實時性又不會把token撐得很大。// 偽代碼JWT payload 示例 { sub: u_12345, tenant: tenant_678, roles: [ROLE_MODEL_ADMIN], perm_version: 12, exp: 1710000000 }perm_version是一個自增版本號每次該用戶的角色或權限變更版本號加1緩存里的權限數據也跟著刷新。網關里只用判斷JWT的perm_version和緩存里的版本是否一致不一致就重新加載權限不用重啟服務。4.2 后端權限攔截基于注解加自定義校驗器后端接口權限校驗我推薦直接用Spring Security加方法級注解或者用AOP自定義一個權限注解。Spring Security生態成熟但自定義注解更輕量適合不想被框架綁得太死的團隊。我通常定義一個RequirePermission的注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequirePermission { String value(); }然后在需要權限的接口上標注PostMapping(/v1/model/invoke) RequirePermission(model:invoke:gpt-4o) public Result invokeModel(RequestBody InvokeRequest request) { // 業務邏輯 }再寫一個切面在方法執行前攔截校驗當前用戶是否擁有指定權限點Aspect Component public class PermissionAspect { Around(annotation(requirePermission)) public Object checkPermission(ProceedingJoinPoint joinPoint, RequirePermission requirePermission) throws Throwable { PermissionContext ctx PermissionContextHolder.get(); if (!ctx.hasPermission(requirePermission.value())) { throw new ForbiddenException(requirePermission.value()); } return joinPoint.proceed(); } }這樣做的好處是權限判斷和業務邏輯完全解耦代碼里能看到每個接口明確的權限要求后面接新的AI模型也只需要標注對應的權限點。缺點也有如果一個接口有多個權限點注解只能寫一個這時可以把注解改成支持字符串數組校驗時滿足任一即可或者改成必須全部滿足看業務需要。4.3 前端按鈕級權限控制與菜單動態化后端校驗做完了前端如果不配合體驗會很糟。用戶看到一堆點不進去的菜單肯定會困惑。所以前端也要根據權限點動態控制界面元素。登錄成功后后端返回當前用戶擁有的權限點列表前端存起來。然后寫一個v-permission指令控制按鈕顯隱// Vue 3 指令示例 app.directive(permission, { mounted(el, binding) { const required binding.value; const userPerms store.state.userPerms; if (!userPerms.includes(required)) { el.parentNode el.parentNode.removeChild(el); } } });模板里這樣用el-button v-permissionmodel:invoke:gpt-4o調用GPT-4o/el-button菜單動態化同理后端返回菜單樹的時候每個菜單項都綁定權限點編碼前端渲染前先過濾一遍沒有權限的菜單直接不渲染。這里強烈建議前后端權限點編碼完全一致不要前端一套、后端一套否則排查起來要命。4.4 緩存權限Redis加速與一致性權限校驗走數據庫是能跑但高并發下數據庫壓力太大。我建議把用戶權限點列表緩存到Redis里key類似perm:user:{userId}value是權限點集合的JSON數組。緩存之后要考慮失效問題。權限變更時除了更新數據庫還要主動刪除Redis緩存。由于JWT里有perm_version也可以約定每次請求帶著版本號網關發現版本落后就主動刷新這樣即使Redis被誤刪也能自動重建。不過緩存只是加速手段不能作為唯一數據源。高安全場景下寫操作確認權限前最好回源數據庫校驗一次權限避免緩存數據被篡改導致越權。大多數平臺沒那么高要求Redis緩存就夠了。5. 常見問題與排查技巧實錄5.1 改了角色權限用戶還是能訪問舊功能這是最常遇到的問題十有八九是緩存或token導致的。JWT里塞了權限列表的token沒過期之前權限當然不變Redis緩存沒刪的同樣會讀到舊權限。排查思路三步走第一步看JWT里有沒有權限數據第二步看Redis緩存里有沒有舊數據第三步看權限變更接口有沒有主動刪緩存。我見過一個團隊的問題是權限變更接口改了數據庫但緩存刪除代碼在一個新加的事務里事務回滾了緩存卻被刪了于是權限刷新和數據庫狀態不一致排查了整整一下午。所以緩存和數據庫的操作順序一定要設計好我的習慣是先更新數據庫再刪緩存緩存刪除失敗要做重試。5.2 并發扣費導致配額超扣上文提到的并發問題我再展開講一個真實案例。我們上線初期某個客戶用腳本并發調用了20次接口結果余額從1000被扣到負數雖然模型調用全成功了但對賬就是不對。根因還是“先查余額再扣費”不是原子操作。用數據庫樂觀鎖能修但性能差一點。我后來改用了Redis的Lua腳本把“檢查余額、扣減、返回剩余”放在一個腳本里執行Redis保證腳本原子性徹底解決了并發問題。如果你們沒有Redis也可以用數據庫原子更新比如UPDATE account SET credits credits - 20 WHERE credits 20受影響的記錄數為0就說明余額不足這個SQL本身是原子的。5.3 角色多、權限矩陣亂怎么治理正常來說權限點控制在100個以內角色控制在20個以內維護起來問題不大。一旦角色超過30個權限矩陣基本就會失控。我的建議是定期做角色收斂。把權限點高度重疊的角色合并用用戶組而不是多角色來解決“一批人總是擁有相同角色”的需求。另外每次給角色加權限點時都要反問一句“這個權限真的需要放到角色上嗎能不能用ABAC規則做臨時開通”權限點不是越多越好每多一個權限點就多一分越權的可能。5.4 多租戶數據越權訪問全局攔截器兜底多租戶SaaS最危險的場景就是A租戶的用戶傳入B租戶的資源ID接口如果沒有做租戶隔離數據就泄露了。光靠開發人員在SQL里寫where tenant_id ?往往不夠因為人都會忘。我建議做一個全局MyBatis攔截器或者在ORM層統一注入租戶條件。比如MyBatis-Plus就有租戶插件配置好tenant_id字段后所有自動拼接的SQL都會帶上租戶條件。這樣即便開發者漏寫了框架也會兜底。前提是你能接受所有表都有tenant_id字段這個設計需要前期規劃好后期臨時加非常痛苦。5.5 權限點編碼錯誤排查還有一個細節權限點編碼是字符串一旦寫錯比如前端要求model:invoke:gpt-4o后端接口標的是model:invoke:gpt4o用戶就會莫名看到按鈕被隱藏或者接口403。這種問題沒有好辦法只能靠規范。我建議權限點編碼統一用常量類或枚舉管理前后端從接口文檔自動生成類型定義避免手敲出錯。審核代碼的時候重點看權限點字符串是否全部來自常量引用而不是隨手寫的字面量。如果讓我重新把權限體系再做一遍我會把租戶隔離和審計日志放到最高優先級因為這兩個東西一旦上線后想補成本比角色模型大多了。RBAC的核心思路不復雜復雜的是把“誰能在什么條件下做什么事”這個一句話需求拆成用戶、角色、權限點、數據范圍、配額、審計這六個維度并且讓它們各司其職又彼此協作。希望這篇實戰記錄能給你一些參考。