「權限判斷不就是 if ($user->role === 'admin') 這種條件式嗎?寫在哪裡不都一樣?」
寫在 Controller 裡確實能動,但問題是同一筆資料的權限判斷,往往不會只在一個地方需要用到——列表頁要判斷能不能看到某筆資料、編輯頁要判斷能不能改、API 也要判斷能不能刪除。今天要看的是 Laravel 的 Policy 機制,怎麼把「誰能對這筆資料做什麼」的判斷邏輯,集中收斂進一個地方。
Gate/$user->can() 串起來Laravel 的 Policy 是一個純 PHP 類別,跟某個特定 Model 對應。這個系統裡有一支很單純的 BannerPolicy,管理「輪播圖」這個資料的權限:
class BannerPolicy
{
public function viewAny(User $user): ?bool
{
return $user->can('ViewAny:BannerCategory');
}
public function view(User $user, Banner $banner): ?bool
{
return $user->can('View:BannerCategory');
}
public function create(User $user): ?bool
{
return $user->can('Create:BannerCategory');
}
public function update(User $user, Banner $banner): ?bool
{
return $user->can('Update:BannerCategory');
}
public function delete(User $user, Banner $banner): ?bool
{
return $user->can('Delete:BannerCategory');
}
public function deleteAny(User $user): ?bool
{
return $user->can('DeleteAny:BannerCategory');
}
}
方法名稱本身就是 Laravel Policy 的慣例:viewAny(能不能看列表)、view(能不能看單筆)、create(能不能新增)、update(能不能改)、delete(能不能刪單筆)、deleteAny(能不能批次刪除)。這幾個方法名稱,Laravel 框架本身認得,會在對應的授權檢查時自動呼叫正確的那一個。
Policy 定義好之後,程式碼裡有兩種常見的呼叫方式:
// 方式一:透過 Gate facade
if (Gate::allows('view', $banner)) {
// ...
}
// 方式二:透過 User model 的 can() 方法(更常見)
if ($user->can('view', $banner)) {
// ...
}
Laravel 會依照傳入的 Model 型別(這裡是 Banner),自動找到對應的 BannerPolicy,呼叫裡面同名的方法。這代表呼叫端完全不需要知道「權限判斷邏輯實際寫在哪一支類別裡」,只要傳對 Model 跟方法名稱,Laravel 自己會找到對的地方去判斷。
注意 BannerPolicy 每個方法裡面,並不是直接寫死判斷邏輯(例如 $user->role === 'admin'),而是又呼叫了另一層 $user->can('ViewAny:BannerCategory')——這是這個系統採用的權限套件慣例:每個角色會被指派一組字串格式的權限名稱(例如 ViewAny:BannerCategory),這些字串儲存在資料庫裡,跟角色一一對應。
這種設計把「判斷邏輯」(Policy 方法)跟「權限資料」(角色被指派了哪些字串權限)分成兩層——Policy 方法只負責回答「這個動作需要哪個權限字串」,至於「這個使用者有沒有這個權限字串」則是委派給底層的權限套件去查資料庫判斷。好處是新增一個角色、調整一個角色能做什麼,只需要改資料庫裡的權限指派,完全不用改任何一行 Policy 程式碼。
// BannerController
public function update(Request $request, Banner $banner)
{
if ($request->user()->role !== 'admin' && $request->user()->role !== 'editor') {
abort(403);
}
// ...
}
// 另一個地方,同樣的判斷邏輯又寫一次,而且可能忘了同步更新
// BannerController
public function update(Request $request, Banner $banner)
{
$this->authorize('update', $banner);
// ...
}
$this->authorize() 是 Controller 裡呼叫 Policy 的便利方法,找不到權限會自動丟出 403 例外,不需要手動寫 abort(403)。
?bool 當回傳型別注意 Policy 方法的回傳型別是 ?bool(可為 null 的布林),不是單純的 bool。這是 Laravel Policy 的一個約定:回傳 true 明確允許、回傳 false 明確拒絕、回傳 null 則代表「這個 Policy 不做判斷,交給下一層(例如 Gate::before() 這種全域授權鉤子)決定」。這個彈性讓 Policy 可以只處理它明確知道的情況,把「這個使用者是不是最高權限,直接放行所有動作」這種全域規則留給別的地方統一處理。
你的專案裡,權限判斷邏輯目前是寫在 Controller 裡,還是收斂進了 Policy?如果同一筆資料的權限判斷,在超過一個地方各自寫了一次,會不會有一天忘記同步更新其中一處?
viewAny/view/create/update/delete)是框架認得的慣例$user->can() 或 $this->authorize(),不需要知道判斷邏輯實際寫在哪一支類別?bool 回傳型別讓 Policy 可以只處理明確情況,把不確定的情況留給全域授權規則決定明天要看一個更進階的授權案例——為什麼系統裡權限最高的角色,不能靠一般的權限字串判斷來保護,需要額外一層寫死的檢查。