用戶授權(quán)
用戶授權(quán)
用戶授權(quán)
簡(jiǎn)介
除了提供開箱即用的 用戶認(rèn)證服務(wù)外,Laravel 還提供了一種簡(jiǎn)單的方法來(lái)處理用戶的授權(quán)動(dòng)作。與用戶認(rèn)證一樣,Laravel 的授權(quán)方法很簡(jiǎn)單,授權(quán)操作有兩種主要方式:gates 和策略。
可以把 gates 和策略比作路由和控制器。Gates 提供了一種簡(jiǎn)單的基于閉包的授權(quán)方法,而策略和控制器類似,圍繞特定模型或資源對(duì)其邏輯進(jìn)行分組來(lái)實(shí)現(xiàn)授權(quán)認(rèn)證。我們先探索 gates,然后研究策略。
在構(gòu)建一個(gè)應(yīng)用的時(shí)候,不用在專門使用 gates 或者只使用策略之間進(jìn)行選擇。大部分應(yīng)用很可能同時(shí)包含 gates 和策略, 并且能夠很好的進(jìn)行工作。 Gates 大部分應(yīng)用在模型和資源沒(méi)有關(guān)系的地方,比如查看管理員的面板。與之相反,策略應(yīng)該在特定的模型或者資源中使用。
Gates
編寫 Gates
Gates 是用來(lái)決定用戶是否授權(quán)執(zhí)行給予動(dòng)作的一個(gè)閉包函數(shù),并且典型的做法就是在App\Providers\AuthServiceProvider
中使用 Gate
來(lái)定義. Gates 總是接收一個(gè)用戶實(shí)例作為第一個(gè)參數(shù),并且可以接收可選參數(shù),比如相關(guān)的 Eloquent 模型:
/** * 注冊(cè)任意用戶認(rèn)證、用戶授權(quán)服務(wù)。 * * @return void */ public function boot(){ $this->registerPolicies(); Gate::define('update-post', function ($user, $post) { return $user->id == $post->user_id; }); }
Gates 也可以使用類似控制器方法 Class@method
風(fēng)格的回調(diào)字符串來(lái)定義:
/** * 注冊(cè)任意用戶認(rèn)證、用戶授權(quán)服務(wù)。 * * @return void */ public function boot(){ $this->registerPolicies(); Gate::define('update-post', 'App\Policies\PostPolicy@update'); }
資源 Gates
你還可以使用 resource
方法去一次性的定義多個(gè) Gate 方法:
Gate::resource('posts', 'App\Policies\PostPolicy');
上面的手動(dòng)定義和以下的 Gate 定義效果是相同的:
Gate::define('posts.view', 'App\Policies\PostPolicy@view'); Gate::define('posts.create', 'App\Policies\PostPolicy@create'); Gate::define('posts.update', 'App\Policies\PostPolicy@update'); Gate::define('posts.delete', 'App\Policies\PostPolicy@delete');
默認(rèn)情況下將會(huì)定義 view
, create
, update
, 和 delete
方法。通過(guò)將一個(gè)數(shù)組作為第三個(gè)參數(shù)傳給 resource
方法。你可以覆蓋或者添加到默認(rèn)的方法中。數(shù)組的鍵定義能力的名稱,值定義方法的名稱。例如,下面的代碼將創(chuàng)建兩個(gè)新的 Gate 定義 - posts.image
和 posts.photo
:
Gate::resource('posts', 'PostPolicy', [ 'image' => 'updateImage', 'photo' => 'updatePhoto', ]);
授權(quán)動(dòng)作
使用 gates 來(lái)授權(quán)動(dòng)作的時(shí)候, 你應(yīng)該使用 allows
或者 denies
方法。 注意,不需要將當(dāng)前已認(rèn)證用戶傳遞給這些方法。 Laravel 會(huì)自動(dòng)處理好已經(jīng)認(rèn)證通過(guò)的用戶,然后傳遞給 gete 閉包函數(shù):
if (Gate::allows('update-post', $post)) { // 指定當(dāng)前用戶可以進(jìn)行更新... } if (Gate::denies('update-post', $post)) { // 指定當(dāng)前用戶不能更新... }
如果你想判斷一個(gè)特定的用戶是否已經(jīng)被授權(quán)訪問(wèn)某個(gè)動(dòng)作, 你可以使用在 Gate
在 facade 的 forUser
方法:
if (Gate::forUser($user)->allows('update-post', $post)) { // 用戶可以更新... } if (Gate::forUser($user)->denies('update-post', $post)) { // 用戶不能更新... }
Gate 攔截檢查
有時(shí),你可能希望將所有能力授予特定用戶。所以你可以在所有其他授權(quán)檢查之前使用 before
方法來(lái)定義運(yùn)行的回調(diào):
Gate::before(function ($user, $ability) { if ($user->isSuperAdmin()) { return true; } });
如果 before
回調(diào)方法返回的是非 null 的結(jié)果,則結(jié)果將被視為檢查結(jié)果。
在每次授權(quán)檢查后你可以使用 after
方法定義要執(zhí)行的回調(diào)。 但是,你不能從 after
回調(diào)方法中修改授權(quán)檢查的結(jié)果:
Gate::after(function ($user, $ability, $result, $arguments) { // });
與 before
檢查類似,如果 after
回調(diào)返回非 null 結(jié)果,則結(jié)果將被視為檢查結(jié)果。
創(chuàng)建策略
生成策略
策略是在特定模型或者資源中組織授權(quán)邏輯的類。例如,你的應(yīng)用是一個(gè)博客,那么你在創(chuàng)建或者更新博客的時(shí)候,你可能會(huì)有一個(gè) Post
模型和一個(gè)對(duì)應(yīng)的 PostPolicy
來(lái)授權(quán)用戶動(dòng)作。
你可以使用 artisan 命令
artisan command 中的 make:policy
artisan command 命令來(lái)生成策略。 生成的策略將放置在 app/Policies
目錄中。如果在你的應(yīng)用中不存在這個(gè)目錄,那么 Laravel 將會(huì)為你自動(dòng)生成:
php artisan make:policy PostPolicy
make:policy
命令會(huì)生成一個(gè)空的策略類。如果你想生成的類包含基本的 「CRUD」策略方法,你可以在執(zhí)行命令的時(shí)候指定 --model
這個(gè)選項(xiàng):
php artisan make:policy PostPolicy --model=Post
{tip} 所有的策略會(huì)通過(guò) Laravel 的 服務(wù)容器,來(lái)解析,允許你在策略構(gòu)造器中對(duì)任何需要的依賴使用類型提示,并且自動(dòng)注入。
注冊(cè)策略
一旦策略存在,它就需要進(jìn)行注冊(cè)。新的 Laravel 應(yīng)用中包含的 AuthServiceProvider
有一個(gè) policies
屬性,可以將各種模型對(duì)應(yīng)到它們的策略中。注冊(cè)一個(gè)策略將引導(dǎo) Laravel 在授權(quán)動(dòng)作訪問(wèn)指定模型的時(shí)候使用哪種策略:
<?php namespace App\Providers; use App\Post;use App\Policies\PostPolicy; use Illuminate\Support\Facades\Gate; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; class AuthServiceProvider extends ServiceProvider{ /** * 應(yīng)用的策略映射。 * * @var array */ protected $policies = [ Post::class => PostPolicy::class, ]; /** * 注冊(cè)任意應(yīng)用認(rèn)證、應(yīng)用授權(quán)服務(wù) * * @return void */ public function boot() { $this->registerPolicies(); // } }
策略自動(dòng)發(fā)現(xiàn)
只要模型和策略遵循標(biāo)準(zhǔn)的 Laravel 命名約定,Laravel 就可以自動(dòng)發(fā)現(xiàn)策略,而不是手動(dòng)注冊(cè)模型策略。具體而言,策略必須位于包含模型的目錄下的 Policies
目錄中。因此,例如模型可以放在 app
目錄中,而策略可以放在 app/Policies
目錄中。此外,策略名稱必須與模型名稱匹配,并具有 Policy
后綴。因此,User
模型將對(duì)應(yīng)于 UserPolicy
類。
如果您想提供自己的策略發(fā)現(xiàn)邏輯,可以使用 Gate :: guessPolicyNamesUsing
方法注冊(cè)自定義回調(diào)。通常,應(yīng)該從應(yīng)用程序的 AuthServiceProvider
的 boot
方法調(diào)用此方法:
use Illuminate\Support\Facades\Gate; Gate::guessPolicyNamesUsing(function ($modelClass) { // return policy class name... });
{note} 在
AuthServiceProvider
中顯式映射的任何策略都將優(yōu)先于自動(dòng)發(fā)現(xiàn)策略。
編寫策略
策略方法
一旦授權(quán)策略被注冊(cè),你就可以為授權(quán)過(guò)后的每個(gè)動(dòng)作添加方法。比如,我們?cè)?PostPolicy
中定義一個(gè) update
方法,它會(huì)判斷指定的 User
是否可以更新指定的 Post
實(shí)例。
update
方法接收 User
和 Post
實(shí)例作為參數(shù),并且應(yīng)該返回 true
或者 false
來(lái)表明用戶是否被授權(quán)更新指定的 Post
。所以在這個(gè)例子中,我們需要判斷用戶的 id
是否和 post 中的 user_id
匹配。
<?php namespace App\Policies;use App\User; use App\Post;class PostPolicy{ /** * 判斷該方法能否被用戶操作。 * * @param \App\User $user * @param \App\Post $post * @return bool */ public function update(User $user, Post $post) { return $user->id === $post->user_id; } }
你可以繼續(xù)為這個(gè)授權(quán)策略定義額外的方法。比如,你可以定義 view
或者 delete
方法來(lái)授權(quán) Post
的多種行為,還可以為自定義的策略方法起一個(gè)你自己喜歡的名字。
{tip} 如果在 Artisan 控制臺(tái)生成策略時(shí),使用
--model
選項(xiàng),它會(huì)包含進(jìn)去view
,create
,update
和delete
動(dòng)作方法。
不包含模型方法
一些策略方法只接收當(dāng)前認(rèn)證通過(guò)的用戶作為參數(shù),而不用傳入與授權(quán)相關(guān)的模型實(shí)例。最常見(jiàn)的應(yīng)用場(chǎng)景就是授權(quán) create
動(dòng)作。比如,如果你正在創(chuàng)建一篇博客,你可能希望先檢查一下當(dāng)前用戶是否有權(quán)限創(chuàng)建它。
當(dāng)定義一個(gè)不需要傳入模型實(shí)例的策略方法時(shí),比如 create
方法,它就是不接收模型實(shí)例作為參數(shù)。你應(yīng)該定義這個(gè)方法只接收授權(quán)過(guò)的用戶作為參數(shù)。
/** * 判斷用戶是否可以創(chuàng)建請(qǐng)求。 * * @param \App\User $user * @return bool */ public function create(User $user){ // }
訪客用戶
默認(rèn)情況下,如果傳入的 HTTP 請(qǐng)求不是經(jīng)過(guò)身份驗(yàn)證的用戶發(fā)起的,那么所有的 gates 和策略都會(huì)自動(dòng)返回 false
。 然而,你可以允許這些授權(quán)檢查通過(guò)聲明一個(gè)『可選的』類型提示或?yàn)橛脩魠?shù)定義提供 null
默認(rèn)值,從而傳遞到你的 gates 和策略中:
<?php namespace App\Policies; use App\User;use App\Post;class PostPolicy{ /** * 判斷用戶是否能更新指定帖子。 * * @param \App\User $user * @param \App\Post $post * @return bool */ public function update(?User $user, Post $post) { return $user->id === $post->user_id; } }
策略過(guò)濾器
對(duì)特定用戶,你可能希望通過(guò)指定的策略授權(quán)所有動(dòng)作。 要達(dá)到這個(gè)目的,可以在策略中定義一個(gè) before
方法。 before
方法會(huì)在策略中其它所有方法之前執(zhí)行,這樣提供了一種方式來(lái)授權(quán)動(dòng)作而不是指定的策略方法來(lái)執(zhí)行判斷。這個(gè)功能最常見(jiàn)的場(chǎng)景是授權(quán)應(yīng)用的管理員可以訪問(wèn)所有動(dòng)作:
public function before($user, $ability){ if ($user->isSuperAdmin()) { return true; } }
如果你想拒絕某個(gè)用戶所有的授權(quán),你應(yīng)當(dāng)在 before
方法中返回 false
。如果返回值是 null
,那么授權(quán)會(huì)在這個(gè)策略中失敗。
{note} 策略類的
before
方法不會(huì)被調(diào)用,如果該類不包含與被檢查的功能名稱相符的方法。
使用策略授權(quán)動(dòng)作
通過(guò)用戶模型
Laravel 內(nèi)置的 User
模型包含兩個(gè)有用的方法來(lái)授權(quán)動(dòng)作: can
和 cant
。這個(gè) can
方法需要指定授權(quán)的動(dòng)作以及相關(guān)的模型。例如,判斷是否授權(quán)一個(gè)用戶更新指定的 Post
模型:
if ($user->can('update', $post)) { // }
如果指定模型的 「策略已被注冊(cè) 」, can
方法會(huì)自動(dòng)調(diào)用合適的策略并返回一個(gè) boolean 值。如果沒(méi)有策略注冊(cè)到這個(gè)模型,can
方法會(huì)嘗試調(diào)用和指定動(dòng)作名稱相匹配的基于閉包的 Gate。
不需要指定模型的動(dòng)作
記住,一些動(dòng)作,比如 create
并不需要指定模型實(shí)例。在這種情況下,可傳遞一個(gè)類名給 can
方法。這個(gè)類名將被用于判定使用哪種策略授權(quán)動(dòng)作:
use App\Post;if ($user->can('create', Post::class)) { // 執(zhí)行相關(guān)策略中的 "create" 方法... }
通過(guò)中間件
Laravel 包含一個(gè)可以在請(qǐng)求到達(dá)路由或者控制器之前就進(jìn)行動(dòng)作授權(quán)的中間件。默認(rèn)情況下, Illuminate\Auth\Middleware\Authorize
中間件被指定到你的 App\Http\Kernel
類中的 can
鍵上。讓我們用一個(gè)授權(quán)用戶更新博客的例子來(lái)講解一下 can
這個(gè)中間件的使用:
use App\Post;Route::put('/post/{post}', function (Post $post) { // 當(dāng)前用戶可以進(jìn)行更新操作... })->middleware('can:update,post');
在這個(gè)例子中,我們傳給了 can
中間件兩個(gè)參數(shù)。第一個(gè)參數(shù)是需要授權(quán)的動(dòng)作名稱,第二個(gè)參數(shù)是我們希望傳遞給策略方法的路由參數(shù)。在這種情況下,我們使用了「 隱式路由綁定」,一個(gè) Post
模型會(huì)被傳遞給策略方法。如果用戶不被授權(quán)訪問(wèn)指定的動(dòng)作,這個(gè)中間件將會(huì)生成帶有 403
狀態(tài)碼的 HTTP 響應(yīng)。
不需要指定模型的動(dòng)作
同樣,一些像 create
這樣的動(dòng)作可能不需要模型實(shí)例。在這種情況下,你可以傳一個(gè)類名給中間件。當(dāng)授權(quán)這個(gè)動(dòng)作時(shí),這個(gè)類名將被用來(lái)判斷使用哪個(gè)策略:
Route::post('/post', function () { // 當(dāng)前用戶可以進(jìn)行創(chuàng)建操作... })->middleware('can:create,App\Post');
通過(guò)控制器輔助函數(shù)
除了在 User
模型中提供輔助方法以外,Laravel 也為繼承 App\Http\Controllers\Controller
這個(gè)基類的控制器提供了一個(gè)有用的 authorize
方法。就像 can
方法一樣,這個(gè)方法需要接收你想授權(quán)的動(dòng)作和相關(guān)的模型作為參數(shù)。如果這個(gè)動(dòng)作沒(méi)有被授權(quán), authorize
方法會(huì)拋出一個(gè) Illuminate\Auth\Access\AuthorizationException
的異常,然后 Laravel 默認(rèn)的異常處理器會(huì)將這個(gè)異常轉(zhuǎn)化成帶有 403
狀態(tài)碼的 HTTP 響應(yīng)。
<?php namespace App\Http\Controllers; use App\Post;use Illuminate\Http\Request; use App\Http\Controllers\Controller; class PostController extends Controller{ /** * 更新指定博客帖子。 * * @param Request $request * @param Post $post * @return Response * @throws \Illuminate\Auth\Access\AuthorizationException */ public function update(Request $request, Post $post) { $this->authorize('update', $post); // 當(dāng)前用戶可以更新博客... } }
不需要指定模型的動(dòng)作
和之前討論的一樣,一些動(dòng)作,比如 create
并不需要指定模型實(shí)例的動(dòng)作。在這種情況下,你可以傳遞一個(gè)類名給 authorize
方法。當(dāng)授權(quán)這個(gè)動(dòng)作時(shí),這個(gè)類名將被用來(lái)判斷使用哪個(gè)策略:
/** * 創(chuàng)建一個(gè)新的博客 * * @param Request $request * @return Response * @throws \Illuminate\Auth\Access\AuthorizationException */ public function create(Request $request){ $this->authorize('create', Post::class); // 當(dāng)前用戶可以新建博客... }
授權(quán)資源控制器
如果你使用的是 資源控制器,那么你就可以在控制器構(gòu)造方法里使用 authorizeResource
方法。該方法會(huì)把合適 can
中間件附加到資源控制器相應(yīng)的方法中。
authorizeResource
方法接收模板類名作為第一個(gè)參數(shù),包含模型 ID 的路由 / 請(qǐng)求參數(shù)的名稱作為其第二個(gè)參數(shù):
<?php namespace App\Http\Controllers; use App\Post;use Illuminate\Http\Request; use App\Http\Controllers\Controller; class PostController extends Controller{ public function __construct() { $this->authorizeResource(Post::class, 'post'); } }
{提示} 你可以使用帶有
--model
選項(xiàng)的make:policy
命令去快速生成基于給定模型的策略類::php artisan make:policy PostPolicy --model=Post
。
通過(guò) Blade 模板
當(dāng)編寫 Blade 模板時(shí),你可能希望頁(yè)面的指定部分只展示給授權(quán)訪問(wèn)指定動(dòng)作的用戶。比如,你可能希望只展示更新的表單給有權(quán)更新博客的用戶。在這樣情況下,你可以使用 @can
和 @cannot
等一系列指令:
@can('update', $post) <!-- The Current User Can Update The Post --> @elsecan('create', App\Post::class) <!-- The Current User Can Create New Post --> @endcan @cannot('update', $post) <!-- The Current User Can't Update The Post --> @elsecannot('create', App\Post::class) <!-- The Current User Can't Create New Post --> @endcannot
這些指令是編寫 @if
和 @unless
語(yǔ)句的捷徑。 @can
和 @cannot
語(yǔ)句分別轉(zhuǎn)化為以下語(yǔ)句:
@if (Auth::user()->can('update', $post)) <!-- The Current User Can Update The Post --> @endif @unless (Auth::user()->can('update', $post)) <!-- The Current User Can't Update The Post --> @endunless
不依賴模型的動(dòng)作
與大多數(shù)其他授權(quán)方法一樣,如果動(dòng)作不需要模型實(shí)例,則可以將類名傳遞給 @ can
和 @ cannot
指令:
@can('create', App\Post::class) <!-- The Current User Can Create Posts --> @endcan @cannot('create', App\Post::class) <!-- The Current User Can't Create Posts --> @endcannot