跳到主要內容

授權

📝 此頁面為 Laravel 官方文檔的繁體中文翻譯。查看原始英文版本

授權

簡介

除了提供內建的認證服務外,Laravel 還提供了一種簡單的方式來根據給定的資源授權使用者操作。例如,即使使用者已認證,他們可能未被授權更新或刪除由應用程式管理的某些 Eloquent 模型或資料庫記錄。Laravel 的授權功能提供了一種簡單、有組織的方式來管理這些類型的授權檢查。

Laravel 提供了兩種主要的授權操作方式:大門策略。可以將大門和策略想像為路由和控制器。大門提供了一種簡單的、基於閉包的授權方法,而策略像控制器一樣,圍繞特定模型或資源組織邏輯。在本文檔中,我們將首先探討大門,然後研究策略。

建立應用程式時,您不需要在完全使用大門或完全使用策略之間做出選擇。大多數應用程式很可能包含大門和策略的某種混合,這完全沒問題!大門最適用於與任何模型或資源無關的操作,例如查看管理員儀表板。相比之下,當您希望為特定模型或資源授權操作時,應使用策略。

大門

編寫大門

[!WARNING] 大門是學習 Laravel 授權功能基礎知識的好方法;但是,在建立穩健的 Laravel 應用程式時,您應該考慮使用策略來組織您的授權規則。

大門只是決定使用者是否有權執行給定操作的閉包。通常,大門使用 Gate 外觀在 App\Providers\AppServiceProvider 類別的 boot 方法中定義。大門始終接收一個使用者實例作為其第一個參數,並可以選擇性地接收額外的參數(如相關的 Eloquent 模型)。

在此範例中,我們將定義一個大門來判斷使用者是否可以更新給定的 App\Models\Post 模型。大門將透過將使用者的 id 與建立該帖子的使用者的 user_id 進行比較來實現這一點:

use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Facades\Gate;

/**
 * 啟動任何應用程式服務。
 */
public function boot(): void
{
    Gate::define('update-post', function (User $user, Post $post) {
        return $user->id === $post->user_id;
    });
}

像控制器一樣,大門也可以使用類別回呼陣列來定義:

use App\Policies\PostPolicy;
use Illuminate\Support\Facades\Gate;

/**
 * 啟動任何應用程式服務。
 */
public function boot(): void
{
    Gate::define('update-post', [PostPolicy::class, 'update']);
}

授權操作

要使用大門授權操作,您應該使用 Gate 外觀提供的 allowsdenies 方法。請注意,您不需要將當前已認證的使用者傳遞給這些方法。Laravel 將自動負責將使用者傳遞到大門閉包中。通常,在執行需要授權的操作之前,在應用程式的控制器中呼叫大門授權方法:

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;

class PostController extends Controller
{
    /**
     * 更新給定的帖子。
     */
    public function update(Request $request, Post $post): RedirectResponse
    {
        if (! Gate::allows('update-post', $post)) {
            abort(403);
        }

        // 更新帖子...

        return redirect('/posts');
    }
}

如果您希望判斷當前已認證使用者以外的使用者是否有權執行操作,可以使用 Gate 外觀上的 forUser 方法:

if (Gate::forUser($user)->allows('update-post', $post)) {
    // 使用者可以更新帖子...
}

if (Gate::forUser($user)->denies('update-post', $post)) {
    // 使用者無法更新帖子...
}

您可以使用 anynone 方法一次授權多個操作:

if (Gate::any(['update-post', 'delete-post'], $post)) {
    // 使用者可以更新或刪除帖子...
}

if (Gate::none(['update-post', 'delete-post'], $post)) {
    // 使用者無法更新或刪除帖子...
}

授權或拋出異常

如果您希望嘗試授權操作,並在使用者不被允許執行給定操作時自動拋出 Illuminate\Auth\Access\AuthorizationException,可以使用 Gate 外觀的 authorize 方法。AuthorizationException 的實例會自動被 Laravel 轉換為 403 HTTP 回應:

Gate::authorize('update-post', $post);

// 操作已授權...

提供額外上下文

用於授權能力的大門方法(allowsdeniescheckanynoneauthorizecancannot)和授權 Blade 指令@can@cannot@canany)可以接收一個陣列作為其第二個參數。這些陣列元素作為參數傳遞給大門閉包,並可用於在做出授權決策時提供額外的上下文:

use App\Models\Category;
use App\Models\User;
use Illuminate\Support\Facades\Gate;

Gate::define('create-post', function (User $user, Category $category, bool $pinned) {
    if (! $user->canPublishToGroup($category->group)) {
        return false;
    } elseif ($pinned && ! $user->canPinPosts()) {
        return false;
    }

    return true;
});

if (Gate::check('create-post', [$category, $pinned])) {
    // 使用者可以建立帖子...
}

大門回應

到目前為止,我們只研究了返回簡單布林值的大門。但是,有時您可能希望返回更詳細的回應,包括錯誤訊息。為此,您可以從大門返回一個 Illuminate\Auth\Access\Response

use App\Models\User;
use Illuminate\Auth\Access\Response;
use Illuminate\Support\Facades\Gate;

Gate::define('edit-settings', function (User $user) {
    return $user->isAdmin
        ? Response::allow()
        : Response::deny('You must be an administrator.');
});

即使您從大門返回授權回應,Gate::allows 方法仍然會返回一個簡單的布林值;但是,您可以使用 Gate::inspect 方法來獲取大門返回的完整授權回應:

$response = Gate::inspect('edit-settings');

if ($response->allowed()) {
    // 操作已授權...
} else {
    echo $response->message();
}

使用 Gate::authorize 方法時(如果操作未被授權,則拋出 AuthorizationException),授權回應提供的錯誤訊息將被傳播到 HTTP 回應:

Gate::authorize('edit-settings');

// 操作已授權...

自訂 HTTP 回應狀態

當操作被大門拒絕時,返回 403 HTTP 回應;但是,有時返回替代的 HTTP 狀態碼可能很有用。您可以使用 Illuminate\Auth\Access\Response 類別上的 denyWithStatus 靜態構造函數來自訂失敗授權檢查返回的 HTTP 狀態碼:

use App\Models\User;
use Illuminate\Auth\Access\Response;
use Illuminate\Support\Facades\Gate;

Gate::define('edit-settings', function (User $user) {
    return $user->isAdmin
        ? Response::allow()
        : Response::denyWithStatus(404);
});

由於透過 404 回應隱藏資源是 Web 應用程式的常見模式,因此提供了 denyAsNotFound 方法以增加便利性:

use App\Models\User;
use Illuminate\Auth\Access\Response;
use Illuminate\Support\Facades\Gate;

Gate::define('edit-settings', function (User $user) {
    return $user->isAdmin
        ? Response::allow()
        : Response::denyAsNotFound();
});

攔截大門檢查

有時,您可能希望向特定使用者授予所有能力。您可以使用 before 方法來定義一個在所有其他授權檢查之前運行的閉包:

use App\Models\User;
use Illuminate\Support\Facades\Gate;

Gate::before(function (User $user, string $ability) {
    if ($user->isAdministrator()) {
        return true;
    }
});

如果 before 閉包返回一個非空結果,則該結果將被視為授權檢查的結果。

您可以使用 after 方法來定義一個在所有其他授權檢查之後執行的閉包:

use App\Models\User;

Gate::after(function (User $user, string $ability, bool|null $result, mixed $arguments) {
    if ($user->isAdministrator()) {
        return true;
    }
});

after 閉包返回的值不會覆蓋授權檢查的結果,除非大門或策略返回了 null

行內授權

有時,您可能希望判斷當前已認證的使用者是否有權執行給定操作,而無需編寫與該操作對應的專用大門。Laravel 允許您透過 Gate::allowIfGate::denyIf 方法來執行這些類型的「行內」授權檢查。行內授權不會執行任何已定義的「之前」或「之後」授權鉤子

use App\Models\User;
use Illuminate\Support\Facades\Gate;

Gate::allowIf(fn (User $user) => $user->isAdministrator());

Gate::denyIf(fn (User $user) => $user->banned());

如果操作未被授權或當前沒有認證使用者,Laravel 將自動拋出 Illuminate\Auth\Access\AuthorizationException 異常。AuthorizationException 的實例會自動被 Laravel 的異常處理器轉換為 403 HTTP 回應。

建立策略

生成策略

策略是圍繞特定模型或資源組織授權邏輯的類別。例如,如果您的應用程式是一個部落格,您可能有一個 App\Models\Post 模型和一個相應的 App\Policies\PostPolicy 來授權使用者操作(如建立或更新帖子)。

您可以使用 make:policy Artisan 命令來生成策略。生成的策略將放置在 app/Policies 目錄中。如果此目錄在您的應用程式中不存在,Laravel 將為您建立它:

php artisan make:policy PostPolicy

make:policy 命令將生成一個空的策略類別。如果您希望生成一個包含與查看、建立、更新和刪除資源相關的範例策略方法的類別,可以在執行命令時提供 --model 選項:

php artisan make:policy PostPolicy --model=Post

註冊策略

策略發現

預設情況下,Laravel 會自動發現策略,只要模型和策略遵循標準 Laravel 命名慣例。具體來說,策略必須在包含模型的目錄或其上方的 Policies 目錄中。因此,例如,模型可以放在 app/Models 目錄中,而策略可以放在 app/Policies 目錄中。在這種情況下,Laravel 將首先在 app/Models/Policies 中檢查策略,然後在 app/Policies 中檢查。此外,策略名稱必須與模型名稱匹配,並以 Policy 為後綴。因此,User 模型將對應一個 UserPolicy 策略類別。

如果您希望定義自己的策略發現邏輯,可以使用 Gate::guessPolicyNamesUsing 方法註冊一個自訂策略發現回呼。通常,此方法應在應用程式的 AppServiceProviderboot 方法中被呼叫:

use Illuminate\Support\Facades\Gate;

Gate::guessPolicyNamesUsing(function (string $modelClass) {
    // 返回給定模型的策略類別名稱...
});

手動註冊策略

使用 Gate 外觀,您可以在應用程式的 AppServiceProviderboot 方法中手動註冊策略及其對應的模型:

use App\Models\Order;
use App\Policies\OrderPolicy;
use Illuminate\Support\Facades\Gate;

/**
 * 啟動任何應用程式服務。
 */
public function boot(): void
{
    Gate::policy(Order::class, OrderPolicy::class);
}

或者,您可以將 UsePolicy 屬性放在模型類別上以通知 Laravel 該模型的相應策略:

<?php

namespace App\Models;

use App\Policies\OrderPolicy;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Model;

#[UsePolicy(OrderPolicy::class)]
class Order extends Model
{
    //
}

編寫策略

策略方法

註冊策略類別後,您可以為其授權的每個操作添加方法。例如,讓我們在 PostPolicy 上定義一個 update 方法,該方法判斷給定的 App\Models\User 是否可以更新給定的 App\Models\Post 實例。

update 方法將接收一個 User 和一個 Post 實例作為其參數,並應返回 truefalse 來指示使用者是否有權更新給定的 Post。因此,在此範例中,我們將驗證使用者的 id 是否與帖子上的 user_id 匹配:

<?php

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    /**
     * 判斷給定的帖子是否可以被使用者更新。
     */
    public function update(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }
}

您可以根據需要繼續在策略上定義其他方法,以授權各種操作。例如,您可能會定義 viewdelete 方法來授權各種 Post 相關操作,但請記住,您可以自由地為策略方法提供任何您喜歡的名稱。

如果您在透過 Artisan 控制台生成策略時使用了 --model 選項,它已經包含 viewAnyviewcreateupdatedeleterestoreforceDelete 操作的方法。

[!NOTE] 所有策略都透過 Laravel 服務容器解析,允許您在策略的構造函數中鍵入提示所需的任何依賴,以便自動注入它們。

策略回應

到目前為止,我們只研究了返回簡單布林值的策略方法。但是,有時您可能希望返回更詳細的回應,包括錯誤訊息。為此,您可以從策略方法返回一個 Illuminate\Auth\Access\Response 實例:

use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;

/**
 * 判斷給定的帖子是否可以被使用者更新。
 */
public function update(User $user, Post $post): Response
{
    return $user->id === $post->user_id
        ? Response::allow()
        : Response::deny('You do not own this post.');
}

從策略返回授權回應時,Gate::allows 方法仍然會返回一個簡單的布林值;但是,您可以使用 Gate::inspect 方法來獲取大門返回的完整授權回應:

use Illuminate\Support\Facades\Gate;

$response = Gate::inspect('update', $post);

if ($response->allowed()) {
    // 操作已授權...
} else {
    echo $response->message();
}

使用 Gate::authorize 方法時(如果操作未被授權,則拋出 AuthorizationException),授權回應提供的錯誤訊息將被傳播到 HTTP 回應:

Gate::authorize('update', $post);

// 操作已授權...

自訂 HTTP 回應狀態

當操作被策略方法拒絕時,返回 403 HTTP 回應;但是,有時返回替代的 HTTP 狀態碼可能很有用。您可以使用 Illuminate\Auth\Access\Response 類別上的 denyWithStatus 靜態構造函數來自訂失敗授權檢查返回的 HTTP 狀態碼:

use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;

/**
 * 判斷給定的帖子是否可以被使用者更新。
 */
public function update(User $user, Post $post): Response
{
    return $user->id === $post->user_id
        ? Response::allow()
        : Response::denyWithStatus(404);
}

由於透過 404 回應隱藏資源是 Web 應用程式的常見模式,因此提供了 denyAsNotFound 方法以增加便利性:

use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;

/**
 * 判斷給定的帖子是否可以被使用者更新。
 */
public function update(User $user, Post $post): Response
{
    return $user->id === $post->user_id
        ? Response::allow()
        : Response::denyAsNotFound();
}

不帶模型的方法

一些策略方法只接收當前已認證使用者的實例。這種情況在授權 create 操作時最常見。例如,如果您正在建立一個部落格,您可能希望判斷使用者是否有權建立任何帖子。在這些情況下,您的策略方法應該只期望接收一個使用者實例:

/**
 * 判斷給定的使用者是否可以建立帖子。
 */
public function create(User $user): bool
{
    return $user->role == 'writer';
}

訪客使用者

預設情況下,如果傳入的 HTTP 請求不是由已認證的使用者發起的,則所有大門和策略都會自動返回 false。但是,您可以透過聲明「可選」類型提示或為使用者參數定義提供 null 預設值來允許這些授權檢查透過到您的大門和策略:

<?php

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    /**
     * 判斷給定的帖子是否可以被使用者更新。
     */
    public function update(?User $user, Post $post): bool
    {
        return $user?->id === $post->user_id;
    }
}

策略過濾器

對於某些使用者,您可能希望授權給定策略中的所有操作。為此,請在策略上定義一個 before 方法。before 方法將在策略上的任何其他方法之前執行,為您提供在實際呼叫預期策略方法之前授權操作的機會。此功能最常用於授權應用程式管理員執行任何操作:

use App\Models\User;

/**
 * 執行預先授權檢查。
 */
public function before(User $user, string $ability): bool|null
{
    if ($user->isAdministrator()) {
        return true;
    }

    return null;
}

如果您希望拒絕特定類型使用者的所有授權檢查,可以從 before 方法返回 false。如果返回 null,則授權檢查將傳遞到策略方法。

[!WARNING] 如果策略類別不包含與被檢查能力名稱匹配的方法名,則不會呼叫該策略類別的 before 方法。

使用策略授權操作

透過使用者模型

Laravel 應用程式附帶的 App\Models\User 模型包含兩個用於授權操作的有用方法:cancannotcancannot 方法接收您希望授權的操作名稱和相關模型。例如,讓我們判斷使用者是否有權更新給定的 App\Models\Post 模型。通常,這將在控制器方法中完成:

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

class PostController extends Controller
{
    /**
     * 更新給定的帖子。
     */
    public function update(Request $request, Post $post): RedirectResponse
    {
        if ($request->user()->cannot('update', $post)) {
            abort(403);
        }

        // 更新帖子...

        return redirect('/posts');
    }
}

如果為給定模型註冊了策略can 方法將自動呼叫適當的策略並返回布林結果。如果模型沒有註冊策略,can 方法將嘗試呼叫與給定操作名稱匹配的基於閉包的大門。

不需要模型的操作

請記住,一些操作可能對應於不需要模型實例的策略方法(如 create)。在這些情況下,您可以向 can 方法傳遞一個類別名稱。該類別名稱將用於判斷在授權操作時使用哪個策略:

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

class PostController extends Controller
{
    /**
     * 建立一個帖子。
     */
    public function store(Request $request): RedirectResponse
    {
        if ($request->user()->cannot('create', Post::class)) {
            abort(403);
        }

        // 建立帖子...

        return redirect('/posts');
    }
}

透過 Gate 外觀

除了提供給 App\Models\User 模型的有用方法外,您還可以隨時透過 Gate 外觀的 authorize 方法來授權操作。

can 方法一樣,此方法接收您希望授權的操作名稱和相關模型。如果操作未被授權,authorize 方法將拋出 Illuminate\Auth\Access\AuthorizationException 異常,Laravel 異常處理器將自動將其轉換為具有 403 狀態碼的 HTTP 回應:

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;

class PostController extends Controller
{
    /**
     * 更新給定的部落格帖子。
     *
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function update(Request $request, Post $post): RedirectResponse
    {
        Gate::authorize('update', $post);

        // 當前使用者可以更新部落格帖子...

        return redirect('/posts');
    }
}

不需要模型的操作

如前所述,一些策略方法(如 create)不需要模型實例。在這些情況下,您應該向 authorize 方法傳遞一個類別名稱。該類別名稱將用於判斷在授權操作時使用哪個策略:

use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;

/**
 * 建立一個新的部落格帖子。
 *
 * @throws \Illuminate\Auth\Access\AuthorizationException
 */
public function create(Request $request): RedirectResponse
{
    Gate::authorize('create', Post::class);

    // 當前使用者可以建立部落格帖子...

    return redirect('/posts');
}

透過中介層

Laravel 包含一個中介層,可以在傳入請求到達您的路由或控制器之前授權操作。預設情況下,Illuminate\Auth\Middleware\Authorize 中介層可以使用 can中介層別名附加到路由,該別名由 Laravel 自動註冊。讓我們探討一個使用 can 中介層來授權使用者可以更新帖子的範例:

use App\Models\Post;

Route::put('/post/{post}', function (Post $post) {
    // 當前使用者可以更新帖子...
})->middleware('can:update,post');

在此範例中,我們向 can 中介層傳遞了兩個參數。第一個是我們希望授權的操作名稱,第二個是我們希望傳遞給策略方法的路由參數。在這種情況下,由於我們正在使用隱式模型綁定,將向策略方法傳遞一個 App\Models\Post 模型。如果使用者未被授權執行給定操作,中介層將返回一個具有 403 狀態碼的 HTTP 回應。

為了方便,您還可以使用 can 方法將 can 中介層附加到路由:

use App\Models\Post;

Route::put('/post/{post}', function (Post $post) {
    // 當前使用者可以更新帖子...
})->can('update', 'post');

如果您正在使用控制器中介層屬性,可以透過 Authorize 屬性應用 can 中介層:

use Illuminate\Routing\Attributes\Controllers\Authorize;

#[Authorize('update', 'post')]
public function update(Post $post)
{
    // 當前使用者可以更新帖子...
}

不需要模型的操作

同樣,一些策略方法(如 create)不需要模型實例。在這些情況下,您可以向中介層傳遞一個類別名稱。該類別名稱將用於判斷在授權操作時使用哪個策略:

Route::post('/post', function () {
    // 當前使用者可以建立帖子...
})->middleware('can:create,App\Models\Post');

在字串中介層定義中指定完整的類別名稱可能會變得繁瑣。為此,您可以選擇使用 can 方法將 can 中介層附加到路由:

use App\Models\Post;

Route::post('/post', function () {
    // 當前使用者可以建立帖子...
})->can('create', Post::class);

透過 Blade 範本

編寫 Blade 範本時,您可能希望僅在使用者有權執行給定操作時才顯示頁面的一部分。例如,您可能希望僅在使用者可以實際更新帖子時才顯示部落格帖子的更新表單。在這種情況下,您可以使用 @can@cannot 指令:

@can('update', $post)
    
@elsecan('create', App\Models\Post::class)
    
@else
    
@endcan

@cannot('update', $post)
    
@elsecannot('create', App\Models\Post::class)
    
@endcannot

這些指令是編寫 @if@unless 語句的便利捷徑。上面的 @can@cannot 語句等效於以下語句:

@if (Auth::user()->can('update', $post))
    
@endif

@unless (Auth::user()->can('update', $post))
    
@endunless

您還可以判斷使用者是否有權執行給定操作陣列中的任何操作。為此,請使用 @canany 指令:

@canany(['update', 'view', 'delete'], $post)
    
@elsecanany(['create'], \App\Models\Post::class)
    
@endcanany

不需要模型的操作

像大多數其他授權方法一樣,如果操作不需要模型實例,您可以向 @can@cannot 指令傳遞一個類別名稱:

@can('create', App\Models\Post::class)
    
@endcan

@cannot('create', App\Models\Post::class)
    
@endcannot

提供額外上下文

使用策略授權操作時,您可以將一個陣列作為第二個參數傳遞給各種授權函數和輔助函數。陣列中的第一個元素將用於判斷應使用哪個策略,而其餘的陣列元素作為參數傳遞給策略方法,並可用於在做出授權決策時提供額外的上下文。例如,考慮以下 PostPolicy 方法定義,其中包含一個額外的 $category 參數:

/**
 * 判斷給定的帖子是否可以被使用者更新。
 */
public function update(User $user, Post $post, int $category): bool
{
    return $user->id === $post->user_id &&
           $user->canUpdateCategory($category);
}

嘗試判斷已認證的使用者是否可以更新給定帖子時,我們可以像這樣呼叫此策略方法:

/**
 * 更新給定的部落格帖子。
 *
 * @throws \Illuminate\Auth\Access\AuthorizationException
 */
public function update(Request $request, Post $post): RedirectResponse
{
    Gate::authorize('update', [$post, $request->category]);

    // 當前使用者可以更新部落格帖子...

    return redirect('/posts');
}

授權和 Inertia

雖然授權必須始終在伺服器上處理,但為前端應用程式提供授權資料以正確渲染應用程式的 UI 通常會很方便。Laravel 沒有定義向 Inertia 驅動的前端公開授權資訊的必要慣例。

但是,如果您正在使用 Laravel 的基於 Inertia 的入門套件之一,則您的應用程式已經包含一個 HandleInertiaRequests 中介層。在此中介層的 share 方法中,您可以返回將提供給應用程式中所有 Inertia 頁面的共享資料。此共享資料可用作定義使用者授權資訊的便利位置:

<?php

namespace App\Http\Middleware;

use App\Models\Post;
use Illuminate\Http\Request;
use Inertia\Middleware;

class HandleInertiaRequests extends Middleware
{
    // ...

    /**
     * 定義預設共享的屬性。
     *
     * @return array
     */
    public function share(Request $request)
    {
        return [
            ...parent::share($request),
            'auth' => [
                'user' => $request->user(),
                'permissions' => [
                    'post' => [
                        'create' => $request->user()->can('create', Post::class),
                    ],
                ],
            ],
        ];
    }
}