授權
簡介
除了提供內建的認證服務,Laravel 也提供了一種能依給定資源來授權使用者的簡單功能。舉例來說,雖然使用者已登入,但這個使用者可能未被授權可更新或刪除網站所管理的特定的 Eloquent Model 或資料庫記錄。Laravel 的授權功能提供了一種簡單且有條理的方法來管理這些種類的授權。
Laravel 提供了兩種主要方法來授權動作:Gate 與 Policy。可以把 Gate 與 Policy 想成是路由與 Controller。Gate 提供了一種簡單、基於閉包的方法來進行授權;而 Policy 就像 Controller 一樣,可以將授權邏輯依照特定的 Model 或資源來進行分組。在本說明文件中,我們會先來探討 Gate,然後再來看看 Policy。
在製作應用程式時,不需要只使用 Gate 或只使用 Policy。大多數應用程式都在某種程度上組合使用 Gate 與 Policy,這樣完全沒問題!Gate 最適合用來處理與 Model 或資源沒關係的操作,如檢視管理員縱覽頁。相較之下,Policy 則應使用於想授權對特定 Model 或 Resource 的操作時
Gate
撰寫 Gate
Gate 是學習基礎 Laravel 授權功能的最好的方法。但是,在製作大型 Laravel 應用程式時,應考慮通過 Policy 來整理各個授權規則。
Gate 是簡單的閉包,用來判斷使用者是否已被授權執行特定的動作。通常來說,Gate 會在 App\Providers\AuthServiceProvider
類別中的 boot
方法內通過 Gate
Facade 來定義。Gate 會收到一個使用者實體作為其第一個引數,並且可能還會接受到額外的引數,如相關的 Eloquent Model。
在此範例中,我們會定義一個用來判斷使用者能否更新給定 App\Models\Post
Model 的 Gate。這個 Gate 會通過比對使用者的 id
與建立該貼文的 user_id
來進行判斷:
1use App\Models\Post;2use App\Models\User;3use Illuminate\Support\Facades\Gate;45/**6 * Register any authentication / authorization services.7 */8public function boot(): void9{10 $this->registerPolicies();1112 Gate::define('update-post', function (User $user, Post $post) {13 return $user->id === $post->user_id;14 });15}
1use App\Models\Post;2use App\Models\User;3use Illuminate\Support\Facades\Gate;45/**6 * Register any authentication / authorization services.7 */8public function boot(): void9{10 $this->registerPolicies();1112 Gate::define('update-post', function (User $user, Post $post) {13 return $user->id === $post->user_id;14 });15}
就像 Controller 一樣,Gate 也可以通過類別回呼陣列來定義:
1use App\Policies\PostPolicy;2use Illuminate\Support\Facades\Gate;34/**5 * Register any authentication / authorization services.6 */7public function boot(): void8{9 $this->registerPolicies();1011 Gate::define('update-post', [PostPolicy::class, 'update']);12}
1use App\Policies\PostPolicy;2use Illuminate\Support\Facades\Gate;34/**5 * Register any authentication / authorization services.6 */7public function boot(): void8{9 $this->registerPolicies();1011 Gate::define('update-post', [PostPolicy::class, 'update']);12}
授權動作
若要通過 Gate 來授權某個動作,可以使用 Gate
Facade 提供的 allows
或 denies
方法。請注意,不需要將目前已登入的使用者傳給這幾個方法。Laravel 會自動處理好將使用者傳給 Gate 閉包。通常,我們會在 Controller 執行需要授權的特定動作前呼叫這些 Gate 授權方法:
1<?php23namespace App\Http\Controllers;45use App\Http\Controllers\Controller;6use App\Models\Post;7use Illuminate\Http\Request;8use Illuminate\Http\Response;9use Illuminate\Support\Facades\Gate;1011class PostController extends Controller12{13 /**14 * Update the given post.15 */16 public function update(Request $request, Post $post): Response17 {18 if (! Gate::allows('update-post', $post)) {19 abort(403);20 }2122 // 更新貼文...2324 return response()->noContent();25 }26}
1<?php23namespace App\Http\Controllers;45use App\Http\Controllers\Controller;6use App\Models\Post;7use Illuminate\Http\Request;8use Illuminate\Http\Response;9use Illuminate\Support\Facades\Gate;1011class PostController extends Controller12{13 /**14 * Update the given post.15 */16 public function update(Request $request, Post $post): Response17 {18 if (! Gate::allows('update-post', $post)) {19 abort(403);20 }2122 // 更新貼文...2324 return response()->noContent();25 }26}
如果想判斷除了目前登入使用者以外其他的使用者能否執行特定動作,可以使用 Gate
Facade 上的 forUser
方法:
1if (Gate::forUser($user)->allows('update-post', $post)) {2 // 該使用者可更新此貼文...3}45if (Gate::forUser($user)->denies('update-post', $post)) {6 // 該使用者不能更新此貼文...7}
1if (Gate::forUser($user)->allows('update-post', $post)) {2 // 該使用者可更新此貼文...3}45if (Gate::forUser($user)->denies('update-post', $post)) {6 // 該使用者不能更新此貼文...7}
可以通過 any
或 none
方法來在任何時候授權多個動作:
1if (Gate::any(['update-post', 'delete-post'], $post)) {2 // 該使用者可更新或刪除此貼文...3}45if (Gate::none(['update-post', 'delete-post'], $post)) {6 // 該使用者不能更新或刪除此貼文...7}
1if (Gate::any(['update-post', 'delete-post'], $post)) {2 // 該使用者可更新或刪除此貼文...3}45if (Gate::none(['update-post', 'delete-post'], $post)) {6 // 該使用者不能更新或刪除此貼文...7}
授權或擲回例外
如果想在使用者不允許執行給定動作時自動擲回 Illuminate\Auth\Access\AuthorizationException
,可以使用 Gate
Facade 的 authorize
方法。AuthorizationException
的實體會自動被 Laravel 的例外處理常式轉換為 403 HTTP 回應:
1Gate::authorize('update-post', $post);23// 已授權可進行該動作...
1Gate::authorize('update-post', $post);23// 已授權可進行該動作...
提供額外的上下文
用於授權權限的 Gate 方法 (allows
, denies
, check
, any
, none
, authorize
, can
, cannot
) 與授權的 Blade 指示詞 (@can
, @cannot
, @canany
) 都接受一個陣列作為其第二引數。這些陣列的元素會被作為引數傳給 Gate 閉包,並且可在進行權限認證時提供額外的上下文:
1use App\Models\Category;2use App\Models\User;3use Illuminate\Support\Facades\Gate;45Gate::define('create-post', function (User $user, Category $category, bool $pinned) {6 if (! $user->canPublishToGroup($category->group)) {7 return false;8 } elseif ($pinned && ! $user->canPinPosts()) {9 return false;10 }1112 return true;13});1415if (Gate::check('create-post', [$category, $pinned])) {16 // The user can create the post...17}
1use App\Models\Category;2use App\Models\User;3use Illuminate\Support\Facades\Gate;45Gate::define('create-post', function (User $user, Category $category, bool $pinned) {6 if (! $user->canPublishToGroup($category->group)) {7 return false;8 } elseif ($pinned && ! $user->canPinPosts()) {9 return false;10 }1112 return true;13});1415if (Gate::check('create-post', [$category, $pinned])) {16 // The user can create the post...17}
Gate 回應
到目前為止,我們只看過了回傳簡單布林值的 Gate。但,有時候我們可能會想回傳一些更具體的回覆,並在其中包含錯誤訊息。為此,可以在 Gate 內回傳 Illuminate\Auth\Access\Response
:
1use App\Models\User;2use Illuminate\Auth\Access\Response;3use Illuminate\Support\Facades\Gate;45Gate::define('edit-settings', function (User $user) {6 return $user->isAdmin7 ? Response::allow()8 : Response::deny('You must be an administrator.');9});
1use App\Models\User;2use Illuminate\Auth\Access\Response;3use Illuminate\Support\Facades\Gate;45Gate::define('edit-settings', function (User $user) {6 return $user->isAdmin7 ? Response::allow()8 : Response::deny('You must be an administrator.');9});
即使從 Gate 內回傳授權回應,Gate::allows
方法依然會回傳簡單的布林值。不過,可以使用 Gate::inspect
方法來取得 Gate 回傳的完整授權回應:
1$response = Gate::inspect('edit-settings');23if ($response->allowed()) {4 // 已授權可進行該動作...5} else {6 echo $response->message();7}
1$response = Gate::inspect('edit-settings');23if ($response->allowed()) {4 // 已授權可進行該動作...5} else {6 echo $response->message();7}
在使用 Gate::authorize
方法時,若動作未被授權,會回傳 AuthorizationException
。這時,授權回應所提供的錯誤訊息會進一步被傳給 HTTP 回應:
1Gate::authorize('edit-settings');23// 已授權可進行該動作...
1Gate::authorize('edit-settings');23// 已授權可進行該動作...
自定 HTTP Response 狀態
當 Gate 拒絕某個動作時,會回傳 403
HTTP Response。不過,在某些情況下,若能回傳其他的 HTTP 狀態碼更好。我們可以使用 Illuminate\Auth\Access\Response
類別的 denyWithStatus
靜態建構函式來自訂在授權檢查失敗的時候要回傳的 HTTP 狀態碼:
1use App\Models\User;2use Illuminate\Auth\Access\Response;3use Illuminate\Support\Facades\Gate;45Gate::define('edit-settings', function (User $user) {6 return $user->isAdmin7 ? Response::allow()8 : Response::denyWithStatus(404);9});
1use App\Models\User;2use Illuminate\Auth\Access\Response;3use Illuminate\Support\Facades\Gate;45Gate::define('edit-settings', function (User $user) {6 return $user->isAdmin7 ? Response::allow()8 : Response::denyWithStatus(404);9});
而因為使用 404
Response 來隱藏資源是在 Web App 中常見的手段,因此為了方便, Laravel 也提供了 denyAsNotFound
方法:
1use App\Models\User;2use Illuminate\Auth\Access\Response;3use Illuminate\Support\Facades\Gate;45Gate::define('edit-settings', function (User $user) {6 return $user->isAdmin7 ? Response::allow()8 : Response::denyAsNotFound();9});
1use App\Models\User;2use Illuminate\Auth\Access\Response;3use Illuminate\Support\Facades\Gate;45Gate::define('edit-settings', function (User $user) {6 return $user->isAdmin7 ? Response::allow()8 : Response::denyAsNotFound();9});
攔截 Gate 檢查
有的時候,我們可能會想授權特定使用者所有的權限。可以使用 before
方法來定義會在所有權限檢查前執行的閉包:
1use App\Models\User;2use Illuminate\Support\Facades\Gate;34Gate::before(function (User $user, string $ability) {5 if ($user->isAdministrator()) {6 return true;7 }8});
1use App\Models\User;2use Illuminate\Support\Facades\Gate;34Gate::before(function (User $user, string $ability) {5 if ($user->isAdministrator()) {6 return true;7 }8});
若 before
閉包回傳了一個非 null 的結果,則該結果值會被當作權限檢查的結果。
可以使用 after
方法來定義一個會在所有其他權限檢查後執行的閉包:
1use App\Models\User;23Gate::after(function (User $user, string $ability, bool|null $result, mixed $arguments) {4 if ($user->isAdministrator()) {5 return true;6 }7});
1use App\Models\User;23Gate::after(function (User $user, string $ability, bool|null $result, mixed $arguments) {4 if ($user->isAdministrator()) {5 return true;6 }7});
與 before
方法類似,若 after
閉包回傳了非 null 的結果,則該結果會被當作權限檢查的結果。
內嵌授權
有時候,我們可能需要判斷目前登入的使用者是否有權限進行給定動作,但我們不想給這個動作撰寫獨立的 Gate。在 Laravel,我們可以使用 Gate::allowIf
與 Gate::denyIf
方法來進行這類的「內嵌」授權檢查:
1use App\Models\User;2use Illuminate\Support\Facades\Gate;34Gate::allowIf(fn (User $user) => $user->isAdministrator());56Gate::denyIf(fn (User $user) => $user->banned());
1use App\Models\User;2use Illuminate\Support\Facades\Gate;34Gate::allowIf(fn (User $user) => $user->isAdministrator());56Gate::denyIf(fn (User $user) => $user->banned());
若該動作未授權、或是使用者未登入,則 Laravel 會自動擲回 Illuminate\Auth\Access\AuthorizationException
例外。AuthorizationException
實體會自動由 Laravel 的例外處理常式轉換為 403 HTTP 回應。
建立 Policy
產生 Policy
Policy 是用來依照特定 Model 或資源阻止授權邏輯的類別。舉例來說,若你的專案是個部落格,則可能會有 App\Models\Post
Model 以及對應的 App\Policies\PostPolicy
來授權使用者進行建立或更新貼文之類的動作。
可以通過 make:policy
Artisan 指令來產生 Policy。產生的 Policy 會被放在 app/Policies
目錄內。若專案中沒有該目錄中,則 Laravel 會自動建立:
1php artisan make:policy PostPolicy
1php artisan make:policy PostPolicy
make:policy
指令會產生一個空的 Policy 類別。若想產生一個與檢視 (View)、建立 (Create)、更新 (Update)、刪除 (Delete) 資源有關的範例 方法的 Policy 類別,可以在執行指令時提供 --model
選項:
1php artisan make:policy PostPolicy --model=Post
1php artisan make:policy PostPolicy --model=Post
註冊 Policy
建立好 Policy 類別後,需要註冊 Policy。註冊 Policy 就能告訴 Laravel:在進行授權動作時,遇到特定 Model 時要使用哪個對應 Policy。
在新安裝的 Laravel 專案中的 App\Providers\AuthServiceProvider
內,有一個 policies
屬性。該屬性會將 Eloquent Model 映射到其對應的 Policy。通過註冊 Policy,就可以告訴 Laravel 在權限檢查時遇到的 Eloquent Model 要使用哪個 Policy:
1<?php23namespace App\Providers;45use App\Models\Post;6use App\Policies\PostPolicy;7use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;8use Illuminate\Support\Facades\Gate;910class AuthServiceProvider extends ServiceProvider11{12 /**13 * The policy mappings for the application.14 *15 * @var array16 */17 protected $policies = [18 Post::class => PostPolicy::class,19 ];2021 /**22 * Register any application authentication / authorization services.23 */24 public function boot(): void25 {26 $this->registerPolicies();2728 // ...29 }30}
1<?php23namespace App\Providers;45use App\Models\Post;6use App\Policies\PostPolicy;7use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;8use Illuminate\Support\Facades\Gate;910class AuthServiceProvider extends ServiceProvider11{12 /**13 * The policy mappings for the application.14 *15 * @var array16 */17 protected $policies = [18 Post::class => PostPolicy::class,19 ];2021 /**22 * Register any application authentication / authorization services.23 */24 public function boot(): void25 {26 $this->registerPolicies();2728 // ...29 }30}
Policy Auto-Discovery
除了手動註冊 Model Policy,只要 Model 與 Policy 都遵守 Laravel 的命名常規,Laravel 就能自動找到 Policy。講得更仔細一點,各個 Policy 應放置與 Model 目錄下或 Model 目錄上層的 Policies
目錄內。因此,舉例來說,Model 可能被放在 app/Models
目錄內,而模型則可能被放在 app/Policies
目錄內。此時,Laravel 會先在 app/Models/Policies
內檢查 Policy,然後才到 app/Policies
內尋找。另外,Policy 名稱也必須與 Model 名稱相同,然後在後方加上 Policy
後綴。如此一來,User
Model 對應的 Policy 類別就是 UserPolicy
。
若讀者想定義自己的 Policy Discovery 邏輯,可以通過 Gate::guessPolicyNamesUsing
方法來註冊自訂 Policy Discovery 閉包。通常來說,這個方法應該在專案的 AuthServiceProvider
內的 boot
方法內呼叫:
1use Illuminate\Support\Facades\Gate;23Gate::guessPolicyNamesUsing(function (string $modelClass) {4 // 為給定 Model 回傳 Policy 類別的名稱...5});
1use Illuminate\Support\Facades\Gate;23Gate::guessPolicyNamesUsing(function (string $modelClass) {4 // 為給定 Model 回傳 Policy 類別的名稱...5});
所有在 AuthServiceProvider
中顯式映射之 Policy 的優先級都會比 Auto-Discover 的 Policy 高。
撰寫 Policy
Policy 方法
註冊好 Policy 類別後,可以為每個授權的動作加上方法。舉例來說,我們來在 PostPolicy
中定義一個 update
方法,用來判斷給定的 App\Models\User
可否更新給定的 App\Models\Post
實體。
update
方法會在其引數內收到 User
與 Post
實體,並且應回傳 true
或 false
來判斷該使用者是否有權限更新給定的 Post
。因此,在這個例子中,我們會認證使用者的 id
是否與貼文的 user_id
相符:
1<?php23namespace App\Policies;45use App\Models\Post;6use App\Models\User;78class PostPolicy9{10 /**11 * Determine if the given post can be updated by the user.12 */13 public function update(User $user, Post $post): bool14 {15 return $user->id === $post->user_id;16 }17}
1<?php23namespace App\Policies;45use App\Models\Post;6use App\Models\User;78class PostPolicy9{10 /**11 * Determine if the given post can be updated by the user.12 */13 public function update(User $user, Post $post): bool14 {15 return $user->id === $post->user_id;16 }17}
我們可以繼續在 Policy 內為各種所需的權限檢查定義更多額外方法。舉例來說,我們可以定義 view
或 delete
方法來對各種與 Post
有關的動作進行權限檢查。但不要忘了,你可以隨意為 Policy 的方法命名。
通過 Artisan 主控台產生 Policy 時若有使用 --model
選項,則 Policy 就已經包含了 viewAny
, view
, create
, update
, delete
, restore
與 forceDelete
動作的方法。
所有的 Policy 都經由 Laravel 的 Service Container 進行解析,這樣一來,可以在 Policy 的建構函式 (Constructor) 內對任何所需的相依項進行型別提示,這些相依項會被自動插入到 Class 內。
Policy 回應
到目前為止,我們只看過了回傳簡單布林值的 Policy 方法。但,有時候我們可能會想回傳一些更具體的回覆,並在其中包含錯誤訊息。為此,可以在 Policy 方法內回傳 Illuminate\Auth\Access\Response
:
1use App\Models\Post;2use App\Models\User;3use Illuminate\Auth\Access\Response;45/**6 * Determine if the given post can be updated by the user.7 */8public function update(User $user, Post $post): Response9{10 return $user->id === $post->user_id11 ? Response::allow()12 : Response::deny('You do not own this post.');13}
1use App\Models\Post;2use App\Models\User;3use Illuminate\Auth\Access\Response;45/**6 * Determine if the given post can be updated by the user.7 */8public function update(User $user, Post $post): Response9{10 return $user->id === $post->user_id11 ? Response::allow()12 : Response::deny('You do not own this post.');13}
當從 Policy 內回傳授權回應時,Gate::allows
方法依然會回傳簡單的布林值。不過,可以使用 Gate::inspect
方法來取得 Gate 回傳的完整授權回應:
1use Illuminate\Support\Facades\Gate;23$response = Gate::inspect('update', $post);45if ($response->allowed()) {6 // 已授權可進行該動作...7} else {8 echo $response->message();9}
1use Illuminate\Support\Facades\Gate;23$response = Gate::inspect('update', $post);45if ($response->allowed()) {6 // 已授權可進行該動作...7} else {8 echo $response->message();9}
在使用 Gate::authorize
方法時,若動作未被授權,會回傳 AuthorizationException
。這時,授權回應所提供的錯誤訊息會進一步被傳給 HTTP 回應:
1Gate::authorize('update', $post);23// The action is authorized...
1Gate::authorize('update', $post);23// The action is authorized...
自定 HTTP Response 狀態
當 Policy 方法拒絕某個動作時,會回傳 403
HTTP Response。不過,在某些情況下,若能回傳其他的 HTTP 狀態碼更好。我們可以使用 Illuminate\Auth\Access\Response
類別的 denyWithStatus
靜態建構函式來自訂在授權檢查失敗的時候要回傳的 HTTP 狀態碼:
1use App\Models\Post;2use App\Models\User;3use Illuminate\Auth\Access\Response;45/**6 * Determine if the given post can be updated by the user.7 */8public function update(User $user, Post $post): Response9{10 return $user->id === $post->user_id11 ? Response::allow()12 : Response::denyWithStatus(404);13}
1use App\Models\Post;2use App\Models\User;3use Illuminate\Auth\Access\Response;45/**6 * Determine if the given post can be updated by the user.7 */8public function update(User $user, Post $post): Response9{10 return $user->id === $post->user_id11 ? Response::allow()12 : Response::denyWithStatus(404);13}
而因為使用 404
Response 來隱藏資源是在 Web App 中常見的手段,因此為了方便, Laravel 也提供了 denyAsNotFound
方法:
1use App\Models\Post;2use App\Models\User;3use Illuminate\Auth\Access\Response;45/**6 * Determine if the given post can be updated by the user.7 */8public function update(User $user, Post $post): Response9{10 return $user->id === $post->user_id11 ? Response::allow()12 : Response::denyAsNotFound();13}
1use App\Models\Post;2use App\Models\User;3use Illuminate\Auth\Access\Response;45/**6 * Determine if the given post can be updated by the user.7 */8public function update(User $user, Post $post): Response9{10 return $user->id === $post->user_id11 ? Response::allow()12 : Response::denyAsNotFound();13}
沒有 Model 的方法
有些 Policy 方法只會收到目前登入使用者的實體。最常見的情況就是在授權 create
動作時。舉例來說,若正在建立部落格,則可能會想判斷某個使用者是否有權限建立任何貼文。在這種情況想,Policy 方法應該只會收到使用者實體:
1/**2 * Determine if the given user can create posts.3 */4public function create(User $user): bool5{6 return $user->role == 'writer';7}
1/**2 * Determine if the given user can create posts.3 */4public function create(User $user): bool5{6 return $user->role == 'writer';7}
訪客使用者
預設情況下,當連入 HTTP 請求並不是由已登入使用者發起的時候,所有的 Gate 與 Policy 都會回傳 false
。不過,我們可以通過在使用者的引數定義上定義「可選」的型別提示,或是提供一個 null
預設值,來讓這些權限檢查可以進到 Gate 與 Policy 中:
1<?php23namespace App\Policies;45use App\Models\Post;6use App\Models\User;78class PostPolicy9{10 /**11 * Determine if the given post can be updated by the user.12 */13 public function update(?User $user, Post $post): bool14 {15 return optional($user)->id === $post->user_id;16 }17}
1<?php23namespace App\Policies;45use App\Models\Post;6use App\Models\User;78class PostPolicy9{10 /**11 * Determine if the given post can be updated by the user.12 */13 public function update(?User $user, Post $post): bool14 {15 return optional($user)->id === $post->user_id;16 }17}
Policy 篩選器
我們可能會想讓特定使用者擁有某個 Policy 中擁有的所有權限。為此,可以在 Policy 內定義一個 before
方法。before
方法會在 Policy 內任何其他方法之前被執行,如此一來我們便有機會可以在預定的 Policy 方法被實際執行前對該行為進行授權。這個功能最常見的使用情況就是用來授權網站管理員來進行所有動作:
1use App\Models\User;23/**4 * Perform pre-authorization checks.5 */6public function before(User $user, string $ability): bool|null7{8 if ($user->isAdministrator()) {9 return true;10 }1112 return null;13}
1use App\Models\User;23/**4 * Perform pre-authorization checks.5 */6public function before(User $user, string $ability): bool|null7{8 if ($user->isAdministrator()) {9 return true;10 }1112 return null;13}
若拒絕特定類型的使用者的所有授權,可以在 before
方法內回傳 false
。若回傳 null
,則權限檢查會繼續傳到 Policy 方法內。
若 Policy 類別內不含要檢查權限名稱的方法,則 before
方法將不會被呼叫。
通過 Policy 來授權動作
通過 User Model
Laravel 專案內建的 App\Models\User
Model 中包含了兩個實用的方法,可以用來進行權限檢查:can
與 cannot
。can
與 cannot
方法接收用要進行權限檢查的動作名稱,以及相關的 Model。舉例來說,讓我們來判斷某個使用者是否有權限更新給定的 App\Models\Post
Model。一般來說,這個檢查會在 Controller 的方法內進行:
1<?php23namespace App\Http\Controllers;45use App\Http\Controllers\Controller;6use App\Models\Post;7use Illuminate\Http\Request;8use Illuminate\Http\Response;910class PostController extends Controller11{12 /**13 * Update the given post.14 */15 public function update(Request $request, Post $post): Response16 {17 if ($request->user()->cannot('update', $post)) {18 abort(403);19 }2021 // 更新貼文...2223 return response()->noContent();24 }25}
1<?php23namespace App\Http\Controllers;45use App\Http\Controllers\Controller;6use App\Models\Post;7use Illuminate\Http\Request;8use Illuminate\Http\Response;910class PostController extends Controller11{12 /**13 * Update the given post.14 */15 public function update(Request $request, Post $post): Response16 {17 if ($request->user()->cannot('update', $post)) {18 abort(403);19 }2021 // 更新貼文...2223 return response()->noContent();24 }25}
若已為給定的 Model 註冊好 Policy,則 can
方法會自動呼叫適當的 Policy,並回傳布林結果值。若沒有為該 Model 註冊好的 Policy,則 can
方法會呼叫符合給定動作名稱的閉包 Gate。
不需要 Model 的動作
請記得,某些對應到 Policy 方法的動作,如 create
,並不要求 Model 實體。這種情況下,可以將類別名稱傳給 can
方法。類別名稱會用來判斷對動作進行權限檢查時要使用哪個 Policy:
1<?php23namespace App\Http\Controllers;45use App\Http\Controllers\Controller;6use App\Models\Post;7use Illuminate\Http\Request;8use Illuminate\Http\Response;910class PostController extends Controller11{12 /**13 * Create a post.14 */15 public function store(Request $request): Response16 {17 if ($request->user()->cannot('create', Post::class)) {18 abort(403);19 }2021 // 建立貼文...2223 return response()->noContent();24 }25}
1<?php23namespace App\Http\Controllers;45use App\Http\Controllers\Controller;6use App\Models\Post;7use Illuminate\Http\Request;8use Illuminate\Http\Response;910class PostController extends Controller11{12 /**13 * Create a post.14 */15 public function store(Request $request): Response16 {17 if ($request->user()->cannot('create', Post::class)) {18 abort(403);19 }2021 // 建立貼文...2223 return response()->noContent();24 }25}
通過 Controller 輔助函式
除了 App\Models\User
Model 上提供的實用方法外,Laravel 還為所有繼承了 App\Http\Controller
基礎類別的 Controller 提供了一個實用的 authorize
方法。
與 can
方法類似,這個方法接收要進行權限檢查的動作名稱、以及相關的 Model。若該動作未被授權,則 authorize
方法會擲回 Illuminate\Auth\Access\AuthroizationException
例外,Laravel 的例外處理常式會自動將該例外轉成有 403 狀態碼的 HTTP 回應:
1<?php23namespace App\Http\Controllers;45use App\Http\Controllers\Controller;6use App\Models\Post;7use Illuminate\Http\Request;8use Illuminate\Http\Response;910class PostController extends Controller11{12 /**13 * Update the given blog post.14 *15 * @throws \Illuminate\Auth\Access\AuthorizationException16 */17 public function update(Request $request, Post $post): Response18 {19 $this->authorize('update', $post);2021 // 目前使用者可更新此部落格貼文...2223 return response()->noContent();24 }25}
1<?php23namespace App\Http\Controllers;45use App\Http\Controllers\Controller;6use App\Models\Post;7use Illuminate\Http\Request;8use Illuminate\Http\Response;910class PostController extends Controller11{12 /**13 * Update the given blog post.14 *15 * @throws \Illuminate\Auth\Access\AuthorizationException16 */17 public function update(Request $request, Post $post): Response18 {19 $this->authorize('update', $post);2021 // 目前使用者可更新此部落格貼文...2223 return response()->noContent();24 }25}
不需要 Model 的動作
與前面討論過的一樣,某些 Policy 方法,如 create
,並不要求 Model 實體。這種情況下,應將類別名稱傳給 authorize
方法。類別名稱會用來判斷對動作進行權限檢查時要使用哪個 Policy:
1use App\Models\Post;2use Illuminate\Http\Request;3use Illuminate\Http\Response;45/**6 * Create a new blog post.7 *8 * @throws \Illuminate\Auth\Access\AuthorizationException9 */10public function create(Request $request): Response11{12 $this->authorize('create', Post::class);1314 // 目前使用者可建立部落格貼文...1516 return response()->noContent();17}
1use App\Models\Post;2use Illuminate\Http\Request;3use Illuminate\Http\Response;45/**6 * Create a new blog post.7 *8 * @throws \Illuminate\Auth\Access\AuthorizationException9 */10public function create(Request $request): Response11{12 $this->authorize('create', Post::class);1314 // 目前使用者可建立部落格貼文...1516 return response()->noContent();17}
授權 Resource Controller
若使用 Resource Controller,則可以在 Controller 建構函式中使用 authorizeResource
方法。這個方法會將適當的 can
Middleware 定義附加到該 Resource Controller 的方法內。
authorizeResource
方法接受 Model 類別名稱作為其第一個引數,而路由名稱或包含 Model ID 的請求參數將為第二個引數。請先確定 Resource Controller 是使用 --model
旗標建立的,這樣該類別才有所需的方法簽章與型別提示:
1<?php23namespace App\Http\Controllers;45use App\Http\Controllers\Controller;6use App\Models\Post;7use Illuminate\Http\Request;89class PostController extends Controller10{11 /**12 * Create the controller instance.13 */14 public function __construct()15 {16 $this->authorizeResource(Post::class, 'post');17 }18}
1<?php23namespace App\Http\Controllers;45use App\Http\Controllers\Controller;6use App\Models\Post;7use Illuminate\Http\Request;89class PostController extends Controller10{11 /**12 * Create the controller instance.13 */14 public function __construct()15 {16 $this->authorizeResource(Post::class, 'post');17 }18}
下列 Controller 方法會映射到其對應的 Policy 方法。當請求被路由到給定的 Controller 方法時,對應的 Policy 方法會在 Controller 方法執行前被自動叫用:
Controller 方法 | Policy 方法 |
---|---|
index | viewAny |
show | view |
create | create |
store | create |
edit | update |
update | update |
destroy | delete |
可以使用 --model
選項搭配 make:policy
指令來快速為給定的 Model 產生 Policy:php artisan make:policy PostPolicy --model=Post
。
通過 Middleware
Laravel 提供了一個可以用來在連入請求進入路由或 Controller 前進行權限檢查的 Middleware。預設情況下,Illuminate\Auth\Middleware\Authorize
Middleware 在 App\Http\Kernel
類別內被指派到 can
索引鍵上。我們來看看一個使用 can
Middleware 對使用者能否更新貼文進行權限檢查的例子:
1use App\Models\Post;23Route::put('/post/{post}', function (Post $post) {4 // 目前使用者可更新該貼文...5})->middleware('can:update,post');
1use App\Models\Post;23Route::put('/post/{post}', function (Post $post) {4 // 目前使用者可更新該貼文...5})->middleware('can:update,post');
在此例子中,我們將兩個引數傳給了 can
Middleware。第一個引數是我們想進行權限檢查的動作名稱,而第二個引數是我們想傳給 Policy 方法的路由參數。在這個例子中,由於我們使用了隱式 Model 繫結,所以會將 App\Models\Post
Model 傳給 Policy 方法。若使用者沒有權限執行給定的動作,則這個 Middleware 會回傳狀態碼 403 的 HTTP 回應。
為了方便起見,也可以使用 can
方法來將 can
Middleware 附加到路由上:
1use App\Models\Post;23Route::put('/post/{post}', function (Post $post) {4 // 目前使用者可更新該貼文...5})->can('update', 'post');
1use App\Models\Post;23Route::put('/post/{post}', function (Post $post) {4 // 目前使用者可更新該貼文...5})->can('update', 'post');
不需要 Model 的動作
再強調一次,某些 Policy 方法,如 create
,並不要求 Model 實體。這種情況下,可以將類別名稱傳給 Middleware。這個類別名稱會用來判斷對動作進行權限檢查時要使用哪個 Policy:
1Route::post('/post', function () {2 // 目前使用者可建立貼文...3})->middleware('can:create,App\Models\Post');
1Route::post('/post', function () {2 // 目前使用者可建立貼文...3})->middleware('can:create,App\Models\Post');
在字串形式的 Middleware 定義中指定完整的類別名稱可能會有點麻煩。因此,我們也可以使用 can
方法來將 can
Middleware 附加到路由上:
1use App\Models\Post;23Route::post('/post', function () {4 // 目前使用者可建立貼文...5})->can('create', Post::class);
1use App\Models\Post;23Route::post('/post', function () {4 // 目前使用者可建立貼文...5})->can('create', Post::class);
通過 Blade 樣板
在撰寫 Blade 樣板時,我們可能會在使用者有權限執行給定動作時顯示某一部分的頁面。舉例來說,我們可能想在使用者真的可以更新貼文時才顯示更新表單。這時,可以使用 @can
與 @cannot
指示詞:
1@can('update', $post)2 <!-- 目前使用者可以更新貼文... -->3@elsecan('create', App\Models\Post::class)4 <!-- 目前使用者可以建立新貼文... -->5@else6 <!-- ... -->7@endcan89@cannot('update', $post)10 <!-- 目前使用者不能更新貼文... -->11@elsecannot('create', App\Models\Post::class)12 <!-- 目前使用者不能建立新貼文... -->13@endcannot
1@can('update', $post)2 <!-- 目前使用者可以更新貼文... -->3@elsecan('create', App\Models\Post::class)4 <!-- 目前使用者可以建立新貼文... -->5@else6 <!-- ... -->7@endcan89@cannot('update', $post)10 <!-- 目前使用者不能更新貼文... -->11@elsecannot('create', App\Models\Post::class)12 <!-- 目前使用者不能建立新貼文... -->13@endcannot
這些指示詞是撰寫 @if
與 @unless
陳述式時的方便捷徑。上方的 @can
與 @cannot
陳述式與下列陳述式相同:
1@if (Auth::user()->can('update', $post))2 <!-- 目前的使用者可更新該貼文… -->3@endif45@unless (Auth::user()->can('update', $post))6 <!-- 目前的使用者不可更新該貼文… -->7@endunless
1@if (Auth::user()->can('update', $post))2 <!-- 目前的使用者可更新該貼文… -->3@endif45@unless (Auth::user()->can('update', $post))6 <!-- 目前的使用者不可更新該貼文… -->7@endunless
可以在包含一系列動作的陣列中判斷某個使用者是否有權限執行其中的任意動作。為此,請使用 @canany
指示詞:
1@canany(['update', 'view', 'delete'], $post)2 <!-- 目前的使用者可更新、檢視、或刪除該貼文… -->3@elsecanany(['create'], \App\Models\Post::class)4 <!-- 目前的使用者可建立貼文… -->5@endcanany
1@canany(['update', 'view', 'delete'], $post)2 <!-- 目前的使用者可更新、檢視、或刪除該貼文… -->3@elsecanany(['create'], \App\Models\Post::class)4 <!-- 目前的使用者可建立貼文… -->5@endcanany
不需要 Model 的動作
與其他大多數的授權方法一樣,當某個動作不需要 Model 實體時,可以將類別名稱傳給 @can
與 @cannot
指示詞:
1@can('create', App\Models\Post::class)2 <!-- 目前的使用者可建立貼文… -->3@endcan45@cannot('create', App\Models\Post::class)6 <!-- 目前的使用者不可建立貼文… -->7@endcannot
1@can('create', App\Models\Post::class)2 <!-- 目前的使用者可建立貼文… -->3@endcan45@cannot('create', App\Models\Post::class)6 <!-- 目前的使用者不可建立貼文… -->7@endcannot
提供額外的上下文
當使用 Policy 對動作進行權限檢查時,可以將陣列作為第二引數傳給各種權限檢查函式與輔助函式。陣列中的第一個元素是用來判斷要叫用哪個 Policy 的,而剩下的元素則會作為參數傳給 Policy 方法,可用來在做權限檢查時提供額外的上下文。舉例來說,假設有下列 PostPolicy
方法定義,其中包含了一個額外的 $category
參數:
1/**2 * Determine if the given post can be updated by the user.3 */4public function update(User $user, Post $post, int $category): bool5{6 return $user->id === $post->user_id &&7 $user->canUpdateCategory($category);8}
1/**2 * Determine if the given post can be updated by the user.3 */4public function update(User $user, Post $post, int $category): bool5{6 return $user->id === $post->user_id &&7 $user->canUpdateCategory($category);8}
在嘗試判斷登入使用者能否更新給定貼文時,我們可以像這樣叫用該 Policy 方法:
1/**2 * Update the given blog post.3 *4 * @throws \Illuminate\Auth\Access\AuthorizationException5 */6public function update(Request $request, Post $post): Response7{8 $this->authorize('update', [$post, $request->category]);910 // 目前使用者可更新部落格貼文...1112 return response()->noContent();13}
1/**2 * Update the given blog post.3 *4 * @throws \Illuminate\Auth\Access\AuthorizationException5 */6public function update(Request $request, Post $post): Response7{8 $this->authorize('update', [$post, $request->category]);910 // 目前使用者可更新部落格貼文...1112 return response()->noContent();13}