Lesson 10: 守門員的權限:授權 - RBAC 與 Policy 設計
在上一堂課,我們學會了如何驗證「你是誰」(Authentication)。但確認身分只是第一步。
想像你是一間銀行的員工。你刷卡通過大門(Authentication 成功),但这不代表你可以直接走進金庫搬錢。「你能做什麼?」 這就是 授權 (Authorization) 的範疇。
對於後端工程師來說,如果說 Authentication 是守大門的警衛,那麼 Authorization 就是每一扇門上的電子鎖與守門員,它決定了請求是否會被拒絕(403 Forbidden)。
核心觀念:Authentication vs. Authorization
| 特性 | Authentication (驗證) | Authorization (授權) |
| 關鍵問題 | 你是誰? (Who are you?) | 你可以做這件事嗎? (Can you do this?) |
| 失敗狀態碼 | 401 Unauthorized (通常指未登入) | 403 Forbidden (已登入但權限不足) |
| 發生時機 | 請求最前端,確認身分 | 確認身分後,執行具體邏輯前 |
| 生活案例 | 出示護照通關 | 持有頭等艙機票進入貴賓室 |
RBAC:角色基礎存取控制 (Role-Based Access Control)
在小型專案中,你可能會寫出這樣的程式碼:
if ($user->id === 1) {
// 允許刪除文章
}
這種寫法難以維護。當系統變大,我們需要更結構化的管理方式。這就是 RBAC。
什麼是 RBAC?
RBAC 的核心概念是:權限不直接綁定在「人」身上,而是綁定在「角色」身上。
User (使用者):系統的操作者。
Role (角色):一組權限的集合(例如:管理員、編輯、一般會員)。
Permission (權限):具體可以執行的動作(例如:刪除文章、編輯商品)。
資料庫設計範例
一個標準的 RBAC 通常涉及五張表(或簡化為三張):
users(使用者)roles(角色)permissions(權限) - 選用,簡單系統可直接定義在 Role 上role_user(使用者與角色的關聯 - 多對多)permission_role(角色與權限的關聯 - 多對多)
透過這種設計,當有新進員工時,你不需要逐一開啟 50 個功能的權限,只需將他指派為「編輯 (Editor)」角色即可。
RBAC 補充:權限粒度與層次
RBAC 核心概念是 權限不綁定個人,而是綁定角色。在實務中,RBAC 的設計還可以進一步補充以下概念:
1. Permission 粒度
粗粒度角色:Admin、Editor、User
適合小型系統或權限需求單純的情境。
細粒度權限:例如「刪除文章」「編輯文章」「審核留言」
好處是更靈活,角色可以組合多個細節權限,不必新增角色就能控制更多行為。
2. 層次化角色 (Hierarchical RBAC)
大型系統中,角色可以繼承權限。
例如:
User < Editor < AdminEditor 自動擁有 User 權限,Admin 擁有 Editor + User 權限
好處:減少重複設定,維護方便
3. 動態角色與條件權限
有些權限需要依情境動態授予,例如:
專案負責人可以編輯專案
訂單管理員可以修改自己部門的訂單
這通常會搭配 Policy 或 Gate 做進一步檢查
總結:RBAC 不只是 User → Role → Permission,更是一個 設計思維,可以控制粒度、層次與動態條件。
Policy 設計:將業務邏輯與控制器分離
在現代後端框架(如 Laravel, Django, NestJS)中,我們通常不會在 Controller/Service 裡面寫又臭又長的 if-else 來檢查權限。我們使用 Policy (策略模式)。
Policy 的核心目的,就是將「能不能做 (Can)」的邏輯,從「實際去做 (Do)」的邏輯中拆分出來。
這裡不看code,真的很難繼續講下去,我們來看個code 經典範例吧。
以下範例code由Laravel 12 實現。
情境:部落格文章的修改權限
假設規則如下:
管理員 (Admin) 可以修改任何人的文章。
作者 (Author) 只能修改「自己」寫的文章。
一般人 不能修改文章。
Bad Smell Code
我們當然可以這樣寫
//Service
$post = $this->postRepository->find($postId);
if($user->role == User::ADMIN || $post->created_by == auth()->user()->id){
$this->postRepostory->update($updateData , $post->id);
}else {
abort(403);
}
//Repository
function update($data, $postId)
{
return $this->model::where('id', $postId)->update($data);
}
但有些bad smell 對吧?
很明顯有:可讀性與維護性缺失,業務邏輯 和 授權邏輯 混在一起,程式碼會變得越來越長。
Modify Step
- 新增Policy檔案
php artisan make:policy PostPolicy --model=Post
#--model=Post 可選
- (可選)如果Model File Name沒有符合Laravel auto load
我這裡故意用Postssss當Model Name來說明
//AppServiceProvider
Gate::policy(Postssss::class, Postolicy::class);
//或者
#[UsePolicy(PostPolicy::class)]
class Postssss extends Model
{
}
- Policy Update
public function update(User $user, Post $post)
{
return $user->role === User::ADMIN
|| $post->created_by === $user->id;
}
或者
class PostPolicy
{
/**
* 在所有其他檢查之前執行
* 如果回傳 true,則直接允許;回傳 null 則繼續往下檢查
*/
public function before(User $user, $ability)
{
// 規則 1: Admin 可以修改任何人的文章
if ($user->role === User::ADMIN) {
return true;
}
}
/**
* 判斷 User 是否可以更新 Post
*/
public function update(User $user, Post $post)
{
// 規則 2: 作者只能修改「自己」的文章
// 規則 3: 一般人不能修改 (因為不是 Admin 也不是作者,這裡會回傳 false)
return $user->id === $post->created_by;
}
}
- 修改Service
$post = $this->postRepository->find($postId);
Gate::authorize('update', $post);
$this->postRepository->update($updateData, $postId);
或
這個會回傳 在 Policy中的return
auth()->user()->can('update' , $post);
或
這個會回傳 Auth/Access/Response
$response = Gate::inspect('update' , $post)
$response->allowed()
業務邏輯與權限邏輯分離比較
| 項目 | Service 判斷 | 使用 Policy |
| 權限判斷位置 | Service | Policy(官方建議) |
| 可維護性 | 中等 | 高(集中管理) |
| 擴展角色邏輯 | 需要修改 Service | 修改單一 Policy |
| 可讀性 | 可以,但會累積 | 清楚:Service=流程、Policy=權 |
常見陷阱
隱藏不等於後端安全: 不要以為在前端把「刪除按鈕」藏起來,駭客就刪不掉資料。後端 API 每一支 修改資料的接口都必須有 Authorization 檢查。
IDOR (Insecure Direct Object References): 這是最常見的漏洞。例如 API 是
GET /orders/1234,如果後端只檢查「使用者是否登入」,而沒檢查「訂單 1234 是否屬於該使用者」,那麼惡意使用者可以遍歷 ID 偷看別人的訂單。Policy 就是防止 IDOR 的最佳防線。超級管理員 (Super Admin): 設計 Policy 時,可以設計一個
before過濾器,讓 Super Admin 繞過所有檢查,避免你在開發時把自己鎖在外面。

