跳至內容

授權

簡介

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

Laravel 提供了兩種主要的授權操作方式:閘道策略。將閘道和策略視為路由和控制器。閘道提供了一種簡單、基於閉包的授權方法,而策略(如控制器)則將邏輯分組在特定的模型或資源周圍。在本文件中,我們將首先探討閘道,然後檢視策略。

在建置應用程式時,您不需要選擇專門使用閘道或專門使用策略。大多數應用程式很可能包含一些閘道和策略的混合,這完全沒問題!閘道最適用於與任何模型或資源無關的操作,例如檢視管理員儀表板。相反地,當您希望授權特定模型或資源的操作時,應使用策略。

閘道

撰寫閘道

exclamation

閘道是學習 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;
 
/**
* Bootstrap any application services.
*/
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;
 
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Gate::define('update-post', [PostPolicy::class, 'update']);
}

授權動作

若要使用閘道授權操作,您應使用 Gate 門面提供的 allowsdenies 方法。請注意,您不需要將目前已驗證的使用者傳遞給這些方法。Laravel 將自動處理將使用者傳遞到閘道閉包中。通常會在您的應用程式控制器中呼叫閘道授權方法,然後再執行需要授權的操作

<?php
 
namespace App\Http\Controllers;
 
use App\Http\Controllers\Controller;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
 
class PostController extends Controller
{
/**
* Update the given post.
*/
public function update(Request $request, Post $post): RedirectResponse
{
if (! Gate::allows('update-post', $post)) {
abort(403);
}
 
// Update the post...
 
return redirect('/posts');
}
}

如果您想判斷目前已驗證的使用者以外的其他使用者是否被授權執行操作,您可以使用 Gate 門面上的 forUser 方法

if (Gate::forUser($user)->allows('update-post', $post)) {
// The user can update the post...
}
 
if (Gate::forUser($user)->denies('update-post', $post)) {
// The user can't update the post...
}

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

if (Gate::any(['update-post', 'delete-post'], $post)) {
// The user can update or delete the post...
}
 
if (Gate::none(['update-post', 'delete-post'], $post)) {
// The user can't update or delete the post...
}

授權或擲回例外

如果您想嘗試授權操作,並且在使用者未被允許執行給定操作時自動擲回 Illuminate\Auth\Access\AuthorizationException,您可以使用 Gate 門面的 authorize 方法。AuthorizationException 的執行個體會由 Laravel 自動轉換為 403 HTTP 回應

Gate::authorize('update-post', $post);
 
// The action is authorized...

提供額外上下文

用於授權能力的閘道方法 (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])) {
// The user can create the post...
}

閘道回應

到目前為止,我們只檢查了傳回簡單布林值的閘道。但是,有時您可能希望傳回更詳細的回應,包括錯誤訊息。若要執行此操作,您可以從您的閘道傳回 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()) {
// The action is authorized...
} else {
echo $response->message();
}

當使用 Gate::authorize 方法時,如果未授權該操作,則會擲回 AuthorizationException,授權回應提供的錯誤訊息將會傳播到 HTTP 回應

Gate::authorize('edit-settings');
 
// The action is authorized...

自訂 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;
}
});

除非閘道或策略傳回 null,否則 after 閉包傳回的值不會覆寫授權檢查的結果。

內聯授權

有時,您可能希望判斷目前已驗證的使用者是否被授權執行給定操作,而無需撰寫對應於該操作的專用閘道。Laravel 允許您透過 Gate::allowIfGate::denyIf 方法執行這些類型的「內聯」授權檢查。內聯授權不會執行任何已定義的 「before」或「after」授權掛鉤

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) {
// Return the name of the policy class for the given model...
});

手動註冊策略

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

use App\Models\Order;
use App\Policies\OrderPolicy;
use Illuminate\Support\Facades\Gate;
 
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Gate::policy(Order::class, OrderPolicy::class);
}

撰寫策略

策略方法

一旦策略類別註冊完成,您可以為它授權的每個動作新增方法。例如,我們在 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
{
/**
* Determine if the given post can be updated by the user.
*/
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
}

您可以根據需要繼續在策略上定義其他方法,用於其授權的各種動作。例如,您可能會定義 viewdelete 方法來授權各種與 Post 相關的動作,但請記住,您可以隨意為您的策略方法指定任何名稱。

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

lightbulb

所有策略都會透過 Laravel 服務容器解析,允許您在策略的建構子中輸入任何需要的依賴項,以便自動注入它們。

策略回應

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

use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;
 
/**
* Determine if the given post can be updated by the user.
*/
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()) {
// The action is authorized...
} else {
echo $response->message();
}

當使用 Gate::authorize 方法時,如果未授權該操作,則會擲回 AuthorizationException,授權回應提供的錯誤訊息將會傳播到 HTTP 回應

Gate::authorize('update', $post);
 
// The action is authorized...

自訂 HTTP 回應狀態

當透過策略方法拒絕動作時,會返回 403 HTTP 回應;但是,有時返回替代的 HTTP 狀態碼可能會很有用。您可以使用 Illuminate\Auth\Access\Response 類別上的 denyWithStatus 靜態建構子,自訂失敗授權檢查所返回的 HTTP 狀態碼。

use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;
 
/**
* Determine if the given post can be updated by the user.
*/
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;
 
/**
* Determine if the given post can be updated by the user.
*/
public function update(User $user, Post $post): Response
{
return $user->id === $post->user_id
? Response::allow()
: Response::denyAsNotFound();
}

沒有模型的方法

某些策略方法只會接收目前已驗證使用者的實例。這種情況在授權 create 動作時最常見。例如,如果您正在建立一個部落格,您可能會希望判斷使用者是否有權限建立任何文章。在這些情況下,您的策略方法應該只預期接收一個使用者實例。

/**
* Determine if the given user can create posts.
*/
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
{
/**
* Determine if the given post can be updated by the user.
*/
public function update(?User $user, Post $post): bool
{
return $user?->id === $post->user_id;
}
}

策略篩選器

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

use App\Models\User;
 
/**
* Perform pre-authorization checks.
*/
public function before(User $user, string $ability): bool|null
{
if ($user->isAdministrator()) {
return true;
}
 
return null;
}

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

exclamation

如果類別不包含名稱與正在檢查的權限名稱相符的方法,則不會呼叫策略類別的 before 方法。

使用策略授權動作

透過使用者模型

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

<?php
 
namespace App\Http\Controllers;
 
use App\Http\Controllers\Controller;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
 
class PostController extends Controller
{
/**
* Update the given post.
*/
public function update(Request $request, Post $post): RedirectResponse
{
if ($request->user()->cannot('update', $post)) {
abort(403);
}
 
// Update the post...
 
return redirect('/posts');
}
}

如果為給定的模型註冊了策略can 方法將自動呼叫適當的策略並返回布林值結果。如果沒有為模型註冊策略,can 方法將嘗試呼叫符合給定動作名稱的基於閉包的閘道。

不需要模型的動作

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

<?php
 
namespace App\Http\Controllers;
 
use App\Http\Controllers\Controller;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
 
class PostController extends Controller
{
/**
* Create a post.
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->cannot('create', Post::class)) {
abort(403);
}
 
// Create the post...
 
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\Http\Controllers\Controller;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
 
class PostController extends Controller
{
/**
* Update the given blog post.
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(Request $request, Post $post): RedirectResponse
{
Gate::authorize('update', $post);
 
// The current user can update the blog post...
 
return redirect('/posts');
}
}

不需要模型的動作

如先前討論的,某些策略方法(例如 create)不需要模型實例。在這些情況下,您應該將類別名稱傳遞給 authorize 方法。類別名稱將用於判斷在授權動作時要使用的策略。

use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
 
/**
* Create a new blog post.
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function create(Request $request): RedirectResponse
{
Gate::authorize('create', Post::class);
 
// The current user can create blog posts...
 
return redirect('/posts');
}

透過中介層

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

use App\Models\Post;
 
Route::put('/post/{post}', function (Post $post) {
// The current user may update the post...
})->middleware('can:update,post');

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

為了方便起見,您也可以使用 can 方法將 can 中介層附加到您的路由。

use App\Models\Post;
 
Route::put('/post/{post}', function (Post $post) {
// The current user may update the post...
})->can('update', 'post');

不需要模型的動作

再次,某些策略方法(例如 create)不需要模型實例。在這些情況下,您可以將類別名稱傳遞給中介層。類別名稱將用於判斷在授權動作時要使用的策略。

Route::post('/post', function () {
// The current user may create posts...
})->middleware('can:create,App\Models\Post');

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

use App\Models\Post;
 
Route::post('/post', function () {
// The current user may create posts...
})->can('create', Post::class);

透過 Blade 模板

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

@can('update', $post)
<!-- The current user can update the post... -->
@elsecan('create', App\Models\Post::class)
<!-- The current user can create new posts... -->
@else
<!-- ... -->
@endcan
 
@cannot('update', $post)
<!-- The current user cannot update the post... -->
@elsecannot('create', App\Models\Post::class)
<!-- The current user cannot create new posts... -->
@endcannot

這些指令是編寫 @if@unless 陳述式的便捷快捷方式。上面的 @can@cannot 陳述式等同於以下陳述式。

@if (Auth::user()->can('update', $post))
<!-- The current user can update the post... -->
@endif
 
@unless (Auth::user()->can('update', $post))
<!-- The current user cannot update the post... -->
@endunless

您也可以判斷使用者是否有權限執行給定動作陣列中的任何動作。若要完成此操作,請使用 @canany 指令。

@canany(['update', 'view', 'delete'], $post)
<!-- The current user can update, view, or delete the post... -->
@elsecanany(['create'], \App\Models\Post::class)
<!-- The current user can create a post... -->
@endcanany

不需要模型的動作

與大多數其他授權方法類似,如果動作不需要模型實例,您可以將類別名稱傳遞給 @can@cannot 指令。

@can('create', App\Models\Post::class)
<!-- The current user can create posts... -->
@endcan
 
@cannot('create', App\Models\Post::class)
<!-- The current user can't create posts... -->
@endcannot

提供額外上下文

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

/**
* Determine if the given post can be updated by the user.
*/
public function update(User $user, Post $post, int $category): bool
{
return $user->id === $post->user_id &&
$user->canUpdateCategory($category);
}

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

/**
* Update the given blog post.
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(Request $request, Post $post): RedirectResponse
{
Gate::authorize('update', [$post, $request->category]);
 
// The current user can update the blog post...
 
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
{
// ...
 
/**
* Define the props that are shared by default.
*
* @return array<string, mixed>
*/
public function share(Request $request)
{
return [
...parent::share($request),
'auth' => [
'user' => $request->user(),
'permissions' => [
'post' => [
'create' => $request->user()->can('create', Post::class),
],
],
],
];
}
}