# 用戶授權
除了提供開箱即用的[用戶認證](https://laravel-china.org/docs/laravel/5.7/authentication)服務外,Laravel還提供了一種簡單的方法來處理用戶的授權動作。與用戶認證一樣,Laravel的授權方法很簡單,授權操作有兩種主要方式:gates和策略。
可以把 gates 和策略比作路由和控制器。Gates提供了一種簡單的基于閉包的授權方法,而策略和控制器類似,圍繞特定模型或資源對其邏輯進行分組來實現授權認證。我們先探索gates,然后檢查政策。
在構建一個應用的時候,不用在專門使用 gates 或者只使用策略之間進行選擇。大部分應用很可能同時包含 gates 和策略, 并且能夠很好的進行工作。 Gates 大部分應用在模型和資源沒有關系的地方,比如查看管理員的面板。與之相反,策略應該在特定的模型或者資源中使用。
## Gates
### 編寫 Gates
Gates 是用來決定用戶是否授權執行給予動作的一個閉包函數,并且典型的做法就是在
`App\Providers\AuthServiceProvider`中使用`Gate`來定義. Gates 總是接收一個用戶實例作為第一個參數,并且可以接收可選參數,比如相關的 Eloquent 模型:
~~~php
/**
* 注冊任意用戶認證、用戶授權服務。
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Gate::define('update-post', function ($user, $post) {
return $user->id == $post->user_id;
});
}
~~~
Gates 也可以使用類似控制器方法`Class@method`風格的回調字符串來定義:
~~~php
/**
* 注冊任意用戶認證、用戶授權服務。
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Gate::define('update-post', 'App\Policies\PostPolicy@update');
}
~~~
#### 資源 Gates
你還可以使用`resource`方法去一次性的定義多個 Gate 方法:
~~~php
Gate::resource('posts', 'App\Policies\PostPolicy');
~~~
上面的手動定義和以下的 Gate 定義效果是相同的:
~~~php
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');
~~~
默認情況下將會定義`view`,`create`,`update`, 和`delete`方法. 通過將一個數組作為第三個參數傳給`resource`方法. 你可以覆蓋或者添加到默認的方法中。數組的鍵定義能力的名稱,值定義方法的名稱。例如,下面的代碼將創建兩個新的 Gate 定義 -`posts.image`和`posts.photo`:
~~~php
Gate::resource('posts', 'PostPolicy', [
'image' => 'updateImage',
'photo' => 'updatePhoto',
]);
~~~
### 授權動作
使用 gates 來授權動作的時候, 你應該使用`allows`或者`denies`方法。 注意,你并不需要給已經認證通過的用戶傳遞這些方法。 Laravel 會自動處理好已經認證通過的用戶,然后傳遞給 gete 閉包函數:
~~~php
if (Gate::allows('update-post', $post)) {
// 指定當前用戶可以進行更新...
}
if (Gate::denies('update-post', $post)) {
// 指定當前用戶不能更新...
~~~
如果你想判斷一個特定的用戶是否已經被授權訪問某個動作, 你可以使用在`Gate`在facade的`forUser`方法:
~~~php
if (Gate::forUser($user)->allows('update-post', $post)) {
// 用戶可以更新...
}
if (Gate::forUser($user)->denies('update-post', $post)) {
// 用戶不能更新...
}
~~~
#### Gate 攔截檢查
有時,你可能希望將所有能力授予特定用戶。所以你可以在所有其他授權檢查之前使用`before`方法來定義運行的回調:
~~~php
Gate::before(function ($user, $ability) {
if ($user->isSuperAdmin()) {
return true;
}
});
~~~
如果`before`回調方法返回的是非null的結果,則結果將被視為檢查結果。
在每次授權檢查后你可以使用`after`方法定義要執行的回調。 但是,你不能從`after`回調方法中修改授權檢查的結果:
~~~php
Gate::after(function ($user, $ability, $result, $arguments) {
//
});
~~~
## 創建策略
### 生成策略
策略是在特定模型或者資源中組織授權邏輯的類。例如,你的應用是一個博客,那么你在創建或者更新博客的時候,你可能會有一個`Post`模型和一個對應的`PostPolicy`來授權用戶動作。
你可以使用`artisan 命令`[artisan command](https://laravel-china.org/docs/laravel/5.7/artisan)中的`make:policy`[artisan command](https://laravel-china.org/docs/laravel/5.7/artisan)命令來生成策略。 生成的策略將放置在`app/Policies`目錄中. 如果在你的應用中不存在這個目錄,那么 Laravel 將會為你自動生成:
~~~php
php artisan make:policy PostPolicy
~~~
`make:policy`命令會生成一個空的策略類。如果你想生成的類包含基本的 「CRUD」策略方法,你可以在執行命令的時候指定`--model`這個選項:
~~~php
php artisan make:policy PostPolicy --model=Post
~~~
> {tip} 所有的策略會通過 Laravel 的[服務容器](https://laravel-china.org/docs/laravel/5.7/container),來解析,允許你在策略構造器中對任何需要的依賴使用類型提示,并且自動注入。
### 注冊策略
一旦策略存在,它就需要進行注冊。新的 Laravel 應用中包含的`AuthServiceProvider`有一個`policies`屬性,可以將各種模型對應到它們的策略中。注冊一個策略將引導 Laravel 在授權動作訪問指定模型的時候使用哪種策略:
~~~php
<?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
{
/**
* 應用的策略映射。
*
* @var array
*/
protected $policies = [
Post::class => PostPolicy::class,
];
/**
* 注冊任意應用認證、應用授權服務
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
//
}
}
~~~
## 編寫策略
### 策略方法
一旦授權策略被注冊,你就可以為授權過后的每個動作添加方法。比如,我們在`PostPolicy`中定義一個`update`方法,它會判斷指定的`User`是否可以更新指定的`Post`實例。
`update`方法接收`User`和`Post`實例作為參數,并且應該返回`true`或者`false`來表明用戶是否被授權更新指定的`Post`。所以在這個例子中,我們需要判斷用戶的`id`是否和 post 中的`user_id`匹配。
~~~php
<?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;
}
}
~~~
你可以繼續為這個授權策略定義額外的方法。比如,你可以定義`view`或者`delete`方法來授權`Post`的多種行為,還可以為自定義的策略方法起一個你自己喜歡的名字。
> {tip} 如果在 Artisan 控制臺生成策略時,使用`--model`選項,它會包含進去`view`,`create`,`update`和`delete`動作方法。
### 不包含模型方法
一些策略方法只接收當前認證通過的用戶作為參數,而不用傳入與授權相關的模型實例。最常見的應用場景就是授權`create`動作。比如,如果你正在創建一篇博客,你可能希望先檢查一下當前用戶是否有權限創建它。
當定義一個不需要傳入模型實例的策略方法時,比如`create`方法,它就是不接收模型實例作為參數。你應該定義這個方法只接收授權過的用戶作為參數。
~~~php
/**
* 判斷用戶是否可以創建請求。
*
* @param \App\User $user
* @return bool
*/
public function create(User $user)
{
//
}
~~~
### 訪客用戶
默認情況下,如果傳入的 HTTP 請求不是經過身份驗證的用戶發起的,那么所有的 gates 和策略都會自動返回`false`。 然而,你可以允許這些授權檢查通過聲明一個『可選的』類型提示或為用戶參數定義提供`null`默認值,從而傳遞到你的 gates 和策略中:
~~~php
<?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;
}
}
~~~
### 策略過濾器
對特定用戶,你可能希望通過指定的策略授權所有動作。 要達到這個目的,可以在策略中定義一個`before`方法。`before`方法會在策略中其它所有方法之前執行,這樣提供了一種方式來授權動作而不是指定的策略方法來執行判斷。這個功能最常見的場景是授權應用的管理員可以訪問所有動作:
~~~php
public function before($user, $ability)
{
if ($user->isSuperAdmin()) {
return true;
}
}
~~~
如果你想拒絕某個用戶所有的授權,你應當在`before`方法中返回`false`。如果返回值是`null`,那么授權會在這個策略中失敗。
> {note} 策略類的`before`方法不會被調用,如果該類不包含與被檢查的功能名稱相符的方法。
## 使用策略授權動作
### 通過用戶模型
Laravel 內置的`User`模型包含兩個有用的方法來授權動作:`can`和`cant`。這個`can`方法需要指定授權的動作以及相關的模型。例如,判斷是否授權一個用戶更新指定的`Post`模型:
~~~php
if ($user->can('update', $post)) {
//
}
~~~
如果指定模型的 「[策略已被注冊](https://laravel-china.org/docs/laravel/5.7/authorization/2271#registering-policies)」,`can`方法會自動調用合適的策略并返回一個 boolean 值。如果沒有策略注冊到這個模型,`can`方法會嘗試調用和指定動作名稱相匹配的基于閉包的 Gate。
#### 不需要指定模型的動作
記住,一些動作,比如`create`并不需要指定模型實例。在這種情況下,可傳遞一個類名給`can`方法。這個類名將被用于判定使用哪種策略授權動作:
~~~php
use App\Post;
if ($user->can('create', Post::class)) {
// 執行相關策略中的 "create" 方法...
}
~~~
### 通過中間件
Laravel 包含一個可以在請求到達路由或者控制器之前就進行動作授權的中間件。默認情況下,`Illuminate\Auth\Middleware\Authorize`中間件被指定到你的`App\Http\Kernel`類中的`can`鍵上。讓我們用一個授權用戶更新博客的例子來講解一下`can`這個中間件的使用:
~~~php
use App\Post;
Route::put('/post/{post}', function (Post $post) {
// 當前用戶可以進行更新操作...
})->middleware('can:update,post');
~~~
在這個例子中,我們傳給了`can`中間件兩個參數。第一個參數是需要授權的動作名稱,第二個參數是我們希望傳遞給策略方法的路由參數。在這種情況下,我們使用了「[隱式路由綁定](https://laravel-china.org/docs/laravel/5.7/routing#implicit-binding)」,一個`Post`模型會被傳遞給策略方法。如果用戶不被授權訪問指定的動作,這個中間件將會生成帶有`403`狀態碼的 HTTP 響應。
#### 不需要指定模型的動作
同樣,一些像`create`這樣的動作可能不需要模型實例。在這種情況下,你可以傳一個類名給中間件。當授權這個動作時,這個類名將被用來判斷使用哪個策略:
~~~php
Route::post('/post', function () {
// 當前用戶可以進行創建操作...
})->middleware('can:create,App\Post');
~~~
### 通過控制器輔助函數
除了在`User`模型中提供輔助方法以外,Laravel 也為繼承`App\Http\Controllers\Controller`這個基類的控制器提供了一個有用的`authorize`方法。就像`can`方法一樣,這個方法需要接收你想授權的動作和相關的模型作為參數。如果這個動作沒有被授權,`authorize`方法會拋出一個`Illuminate\Auth\Access\AuthorizationException`的異常,然后 Laravel 默認的異常處理器會將這個異常轉化成帶有`403`狀態碼的 HTTP 響應。
~~~php
<?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);
// 當前用戶可以更新博客...
}
}
~~~
#### 不需要指定模型的動作
和之前討論的一樣,一些動作,比如`create`并不需要指定模型實例的動作。在這種情況下,你可以傳遞一個類名給`authorize`方法。當授權這個動作時,這個類名將被用來判斷使用哪個策略:
~~~php
/**
* 創建一個新的博客
*
* @param Request $request
* @return Response
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function create(Request $request)
{
$this->authorize('create', Post::class);
// 當前用戶可以新建博客...
}
~~~
### 通過 Blade 模板
當編寫 Blade 模板時,你可能希望頁面的指定部分只展示給授權訪問指定動作的用戶。比如,你可能希望只展示更新的表單給有權更新博客的用戶。在這樣情況下,你可以使用`@can`和`@cannot`一系列指令:
~~~php
@can('update', $post)
<!-- 當前用戶可以進行更新操作 -->
@elsecan('create', App\Post::class)
<!-- 當前用戶可以進行創建操作 -->
@endcan
@cannot('update', $post)
<!-- 當前用戶不能進行更新操作 -->
@elsecannot('create', App\Post::class)
<!-- 當前用戶不能進行創建操作 -->
@endcannot
~~~
這些指令在編寫`@if`和`@unless`時提供了方便的縮寫。`@can`和`@cannot`各自轉化為如下所示的聲明:
~~~php
@if (Auth::user()->can('update', $post))
<!-- 當前用戶可以進行更新操作 -->
@endif
@unless (Auth::user()->can('update', $post))
<!-- 當前用戶不能進行更新操作 -->
@endunless
~~~
#### 不需要指定模型的動作
和大部分其它的授權方法相似,當動作不需要模型實例時,你可以傳遞一個類名給`@can`和`@cannot`指令:
~~~php
@can('create', App\Post::class)
<!-- 當前用戶可以進行創建操作 -->
@endcan
@cannot('create', App\Post::class)
<!-- 當前用戶不能進行創建操作 -->
@endcannot
~~~