在後端系統的設計中,權限(Authorization)永遠是一個核心命題。
在項目初期,為了追求開發速度,往往容易憑直覺採用一種極其簡單的設計方案。然而,正是這個起初看起來“最快”的方案,往往會成為後期維護中最大的噩夢。
本文將從一個典型的“設計反模式”開始,探討如何構建一個成熟的後端權限防禦體系。
一、 起手式的陷阱:User 表裏的 permissions 字段
在項目剛啓動,用户量較少時,很容易出現如下的代碼設計邏輯:
“既然要控制權限,那就在用户表裏加個字段不就行了?”
於是,數據庫裏出現了這樣一種設計:
// 用户表 (User Table) 的一種糟糕設計
{
"id": 101,
"name": "張三",
"dept": "銷售部",
// 直接把權限以數組或逗號分隔字符串的形式存這裏
"permissions": ["order:view", "order:edit", "report:export"]
}
這種**“直連模式”**看起來簡單直接,不需要額外的表結構。但在架構設計視角下,這是給未來埋下的最大技術負債。這種做法會帶來三大災難:
1. 策略變更時的“批量更新噩夢”
假設公司突然出台一條新規:“出於數據安全考慮,暫時收回所有普通銷售人員的‘導出 Excel’權限。”
如果使用數組存儲,面對的是 2000 個銷售人員,就必須編寫複雜的 SQL 腳本或者後台程序,遍歷這 2000 行數據,解析 JSON,從每個人的 permissions 數組中精準剔除 "report:export" 這個字符串,然後再存回去。
風險極高:這種全表級別的寫操作,很容易誤傷(比如誤刪了銷售經理的權限),且難以回滾。
2. 入職時的“幽靈權限” (Ghost Permissions)
這是權限污染的起點。
新人小王入職銷售部,管理員為了省事,通常會**“照着老員工老李的權限,給小王複製一份”**。
但這不僅複製了職位,更復制了漏洞。老李可能因為兩年前的一個臨時項目,被單獨賦予了“查看財務報表”的特殊權限,項目早已結束,但管理員忘了收回。
結果就是,這份錯誤的權限像病毒一樣傳染給了新人。小王剛入職第一天,在毫不知情的情況下,竟然就已經擁有了不該有的財務查看權。
3. 轉崗時的“權限蠕蟲” (Privilege Creep)
這是最危險的情況。
小王從“銷售部”轉崗去“行政部”。管理員往往容易陷入人性的弱點:
- 只記得加行政部的新權限(因為不加幹不了活);
- 卻不敢或忘了刪銷售部的舊權限(因為怕刪錯了出鍋)。
久而久之,隨着幾次轉崗,老員工身上揹負着銷售、市場、行政三個部門的權限。他並沒有完成身份的切換,而是變成了一個由於管理疏忽導致的**“超級管理員”。這就是安全領域著名的“權限氾濫”**。
二、 基石:RBAC 模型的解耦智慧
為了解決上述“直連模式”的痛點,業界通用的標準解法是 RBAC (Role-Based Access Control)。
RBAC 的核心哲學在於解耦:
- 用户 (User) 屬於 角色 (Role)。
- 角色 (Role) 擁有 權限 (Permission)。
“流水的人員,鐵打的角色。”
當需要收回銷售人員的“導出權限”時,只需要找到 “銷售專員” 這個角色,在後台把它的配置勾選去掉。
1 次操作,0 秒延遲,2000 人即時生效。
三、 防禦體系:權限校驗的“三道門”
擁有了 RBAC 模型只是第一步,它解決了“配置”的問題。關鍵在於請求在系統中流轉時,如何實施攔截。一個優秀的權限架構,通常設計得像一個漏斗,分為三層防護。
第一道門:網關/路由級防護 (The Gatekeeper)
這是請求進入系統的第一站,通常位於 API 網關 或 過濾器 (Filter) 層。
- 職責:做最粗粒度的攔截。
- 攔截對象:沒帶 Token 的、Token 過期的、IP 在黑名單的、或者明明是普通用户卻試圖訪問
/admin/**路徑的。 - 定位:它是公司的“保安”,負責把閒雜人等擋在大樓之外,但不關心具體要去哪個工位。
第二道門:功能級防護 (Functional Permission)
當請求通過了網關,進入到具體的業務代碼(Controller/Service)時,就面臨第二道檢查。
- 職責:控制具體的“動作 (Verb)”。
- 邏輯:它只回答**“能不能做”**。例如,用户是否有權調用“查看詳情”接口?
- 侷限:它就像大樓的門禁,只要你有工牌就能進,但它無法識別你是否**“走錯了別人的辦公室”**。
四、 核心挑戰:從“動作許可”到“數據確權” (第三道門)
“第三道門”,也是本文重點探討的深水區。
很多系統做好了前兩道門,卻依然發生了越權訪問。為什麼?
因為你只驗證了“駕駛證”(功能權限),卻忘了驗證“車鑰匙”(數據確權)。
1. 痛點:當 AOP 遇到“具體數據”
假設有一個接口 GET /callback/{channel_id}。
- 第二道門説:✅ 通過,該用户有
callback:read權限。 - 業務層拿到請求後發現:用户想看
channel_id = 999,但這個 Channel 是屬於其他用户的。
如果在業務代碼裏寫校驗:
// 【反模式】業務邏輯被“確權邏輯”污染
pub async fn get_callback_info(channel: Path<CallbackId>) -> Result<Response> {
// ☠️ 既當爹又當媽:業務代碼還要負責查庫校驗歸屬權
let project = db.find_project_by_channel(&channel).await?;
if project.owner_id != user.id {
return Err(403);
}
// ... 真正的業務邏輯
}
這種寫法讓 Controller 充斥着大量的 if-else 歸屬權判斷,且難以複用。一旦規則變得複雜(例如:不僅要是 Owner,還必須是 Project 的 Member),業務代碼將變得臃腫不堪。
2. 解耦之道:聲明式的數據確權 (Permission Service)
要實現解耦,需要把**“數據歸屬權校驗”從業務邏輯中剝離出來,提升為第三道門**。
它的核心在於動態性:必須提取具體的業務參數(Resource ID),結合數據庫狀態進行實時判定。
實現方案:前置守衞 + 獨立鑑權服務
我們構建一個獨立的 Permission Service,專門回答**“用户 U 對資源 R 是否擁有權限 P”**的問題。通過聲明式註解,將這一邏輯前置。
// 【最佳實踐】業務邏輯完全無感知
#[permission(
// 1. 策略:既要有“讀動作”權限,由於涉及具體數據,還必須是“擁有者”
policy = "admin OR owner",
action = "callback:read",
// 2. 【關鍵點】自動提取具體的數據 ID (車鑰匙)
resource_extractor = "channel"
)]
pub async fn get_callback_info(
app_service: CallbackAppService,
State(_ctx): State<AppContext>,
// 宏會自動解析這個 channel 參數,先丟給 Permission Service 查户口
channel: Path<CallbackId>,
) -> Result<Response> {
// --- 核心優勢 ---
// 到了這裏,意味着三層防禦全部通過:
// 1. 用户身份合法 (Door 1)
// 2. 用户有讀權限 (Door 2)
// 3. 用户是這個 channel 的主人 (Door 3 - 數據確權)
// 開發者只關注:怎麼把數據取出來返回
let r = app_service.get_channel(&channel).await?;
Ok(r)
}
通過註解中的 resource_extractor,我們將數據層的校驗邏輯(比如查 Project 表、查 Member 表)前置到了 Controller 入口。這不僅保護了數據,更防止了業務代碼的腐化。
五、 總結
權限系統的設計,本質上是對業務邊界的數字化映射。
從拒絕“User 表直連權限”的陷阱開始,到建立 RBAC 模型,再到構建層層遞進的“三道門”防護,目標始終如一:
讓業務開發人員只關注業務(Business Logic),而讓權限系統像空氣一樣,看不見,卻無處不在地提供安全保障。
本文由mdnice多平台發佈