iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
Software Development

一套真實運作中的 Laravel 系統,拆解它的原生機制系列 第 8

Day 08:Policy 授權基礎——一份 Policy 檔案怎麼決定誰能看見什麼

  • 分享至 

  • xImage
  •  

前言:權限判斷寫在 Controller 裡,跟寫在 Policy 裡,差在哪?

「權限判斷不就是 if ($user->role === 'admin') 這種條件式嗎?寫在哪裡不都一樣?」

寫在 Controller 裡確實能動,但問題是同一筆資料的權限判斷,往往不會只在一個地方需要用到——列表頁要判斷能不能看到某筆資料、編輯頁要判斷能不能改、API 也要判斷能不能刪除。今天要看的是 Laravel 的 Policy 機制,怎麼把「誰能對這筆資料做什麼」的判斷邏輯,集中收斂進一個地方。

今日目標

  • 認識 Laravel Policy 的基本結構——一個 Model 對應一個 Policy 類別
  • 看一個真實案例:一份完整的 Policy 檔案怎麼組成
  • 理解 Policy 方法怎麼跟 Gate$user->can() 串起來
  • 知道這個系統的 Policy 用了什麼字串命名慣例來組織權限

本文主體

一個 Model 對應一個 Policy

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 方法怎麼被呼叫

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 程式碼。

❌ 權限判斷寫死在 Controller 裡,散落各處

// BannerController
public function update(Request $request, Banner $banner)
{
    if ($request->user()->role !== 'admin' && $request->user()->role !== 'editor') {
        abort(403);
    }
    // ...
}

// 另一個地方,同樣的判斷邏輯又寫一次,而且可能忘了同步更新

✅ 權限判斷集中在 Policy,呼叫端只管傳 Model

// 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?如果同一筆資料的權限判斷,在超過一個地方各自寫了一次,會不會有一天忘記同步更新其中一處?

今日重點回顧

  • Laravel Policy 是一個 Model 對應一個 Policy 類別,方法名稱(viewAnyviewcreateupdatedelete)是框架認得的慣例
  • 呼叫端用 $user->can()$this->authorize(),不需要知道判斷邏輯實際寫在哪一支類別
  • 這個系統把「判斷邏輯」跟「權限資料」分成兩層:Policy 方法只回答需要哪個權限字串,實際判斷委派給底層權限套件查資料庫
  • ?bool 回傳型別讓 Policy 可以只處理明確情況,把不確定的情況留給全域授權規則決定

明日預告

明天要看一個更進階的授權案例——為什麼系統裡權限最高的角色,不能靠一般的權限字串判斷來保護,需要額外一層寫死的檢查。


上一篇
Day 07:Model Factory 語意化設計——用具名 state 表達建立資料的意圖
下一篇
Day 09:角色可見性控制——為什麼最高權限角色不能靠一般權限判斷保護
系列文
一套真實運作中的 Laravel 系統,拆解它的原生機制14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言