[ Laravel 5.8 文件 ] 安全系列 —— 授權
簡介
除了提供開箱即用的認證服務之外,Laravel 還提供了一個簡單的方式來管理授權邏輯以便控制對資源的訪問許可權。和認證一樣,在 Laravel 中實現授權很簡單,主要有兩種方式:Gate 和 Policy。
可以將 Gate 和 Policy 分別看作路由和控制器,Gate 提供了簡單的基於閉包的方式進行授權,而 Policy 和控制器一樣,對特定模型或資源上的複雜授權邏輯進行分組,本著由簡入繁的思路,我們首先來看 Gate,然後再看 Policy。
不要將 Gate 和 Policy 看作互斥的東西,實際上,在大多數應用中我們會混合使用它們,這很有必要,因為 Gate 通常用於與模型或資源無關的許可權,比如訪問管理後臺,與之相反,Policy 則用於對指定模型或資源的動作進行授權。
Gate
編寫 Gate
Gate 是用於判斷使用者是否有權進行某項操作的閉包,通常使用Gate
門面定義在App\Providers\AuthServiceProvider
類中。Gate 總是接收使用者例項作為第一個引數,還可以接收相關的 Eloquent 模型例項作為額外引數:
/** * 註冊任意認證/授權服務. * * @return void */ public function boot() { $this->registerPolicies(); Gate::define('update-post', function ($user, $post) { return $user->id == $post->user_id; }); }
Gate 還可以通過使用Class@method
風格的回撥字串定義,和控制器一樣:
/** * Register any authentication / authorization services. * * @return void */ public function boot() { $this->registerPolicies(); Gate::define('update-post', 'App\Policies\PostPolicy@update'); }
資源 Gate
你還可以使用resource
方法一次定義多個 Gate 許可權:
Gate::resource('posts', 'App\Policies\PostPolicy');
上面的寫法等價於下面的 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');
預設情況下,view
、create
、update
和delete
許可權會被定義,你也可以通過傳遞一個數組作為第三個引數到resource
方法來覆蓋或新增其他許可權。這個陣列的鍵就是許可權名稱,而對應的鍵值就是許可權方法的名稱。例如,下面的程式碼將會建立兩個新的 Gate 定義 ——posts.image
和posts.photo
:
Gate::resource('posts', 'PostPolicy', [ 'image' => 'updateImage', 'photo' => 'updatePhoto', ]);
授權動作
要使用 Gate 授權某個動作,可以使用allows
或denies
方法,需要注意的是你可以不傳使用者例項到這些方法,Laravel 會自動將使用者例項(當前使用者)傳遞到 Gate 閉包:
if (Gate::allows('update-post', $post)) { // 當前使用者可以更新文章... } if (Gate::denies('update-post', $post)) { // 當前使用者不能更新文章... }
注:這種情況下,對於未登入使用者所有許可權校驗都會返回false
。
如果你想要判斷指定使用者(非當前使用者)是否有權進行某項操作,可以使用Gate
門面上的forUser
方法:
if (Gate::forUser($user)->allows('update-post', $post)) { // 當前使用者可以更新文章... } if (Gate::forUser($user)->denies('update-post', $post)) { // 當前使用者不能更新文章... }
攔截 Gate 檢查
有時候,你可能想要分配所有許可權給指定使用者,這可以通過在before
方法中定義一個回撥來實現,該回調會在所有授權檢查之前呼叫:
Gate::before(function ($user, $ability) { if ($user->isSuperAdmin()) { return true; } });
如果before
回撥返回一個非 null 結果,該結果將作為檢查結果返回,不再執行後續檢查。
你還可以使用after
方法定義一個回撥在所有授權檢查之後執行:
Gate::after(function ($user, $ability, $result, $arguments) { if ($user->isSuperAdmin()) { return true; } });
和before
檢查類似,如果after
回撥返回的是非 null 結果,該結果將作為檢查結果返回。
建立 Policy
生成 Policy 類
Policy(策略)是用於組織基於特定模型或資源的授權邏輯類,例如,如果你開發的是一個部落格應用,可以有一個Post
模型和與之對應的PostPolicy
來授權使用者建立或更新部落格的動作。
我們使用Artisan 命令make:policy
來生成一個 Policy 類,生成的 Policy 類位於app/Policies
目錄下,如果這個目錄之前不存在,Laravel 會自動為我們建立:
php artisan make:policy PostPolicy
make:policy
命令會生成一個空的 Policy 類,如果你想要生成一個包含基本 CRUD 策略方法的 Policy 類,在執行該命令的時候可以通過--model
指定相應模型:
php artisan make:policy PostPolicy --model=Post
注:所有策略類都通過服務容器進行解析,這樣在策略類的建構函式中就可以通過型別提示進行依賴注入。
註冊 Policy 類
Policy 類建立之後,需要註冊到容器。Laravel 自帶的AuthServiceProvider
包含了一個policies
屬性來對映 Eloquent 模型及與之對應的 Policy 類。註冊 Policy 將會告知 Laravel 在授權給定模型動作時使用哪一個策略類:
<?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 * @translator laravelacademy.org */ protected $policies = [ Post::class => PostPolicy::class, ]; /** * 註冊任意認證/授權服務. * * @return void */ public function boot() { $this->registerPolicies(); // } }
策略類自動發現
除了手動註冊模型與對應策略類對映關係之外,Laravel 還可以自動發現策略類,只要模型類和策略類遵循標準的 Laravel 命名約定(包含名稱空間)。也就是說,策略類必須存放在Policies
目錄下,同時對應模型類必須存放在包含Policies
的父目錄下。舉個例子,如果模型類位於app
目錄下,則對應策略類必須位於app/Policies
目錄下。此外,策略名必須和模型名匹配然後加上一個Policy
字尾。因此,User
模型對應的策略類是UserPolicy
類。
如果你想要提供自定義的策略類自動發現邏輯,可以使用Gate::guessPolicyNamesUsing
方法註冊一個自定義的回撥,通常,我們在AuthServiceProvider
的boot
方法中呼叫該方法:
use Illuminate\Support\Facades\Gate; Gate::guessPolicyNamesUsing(function ($modelClass) { // 返回策略類名... });
注:任何在AuthServiceProvider
中顯式對映的策略類優先順序要高於那些潛在自動發現的策略類。
編寫 Policy
Policy 方法
Policy 類被註冊後,還要為每個授權動作新增方法,例如,我們為使用者更新Post
例項這一動作在PostPolicy
中定義一個update
方法。
update
方法會接收一個User
例項和一個Post
例項作為引數,並且返回true
或false
以表明該使用者是否有許可權對給定Post
進行更新。因此,在這個例子中,我們驗證使用者的id
和文章對應的user_id
是否匹配:
<?php namespace App\Policies; use App\User; use App\Post; class PostPolicy { /** * 判斷給定文章是否可以被使用者更新. * * @param\App\User$user * @param\App\Post$post * @return bool * @translator laravelacademy.org */ public function update(User $user, Post $post) { return $user->id === $post->user_id; } }
你可以繼續在 Policy 類中為授權的許可權定義更多需要的方法,例如,你可以定義view
或者delete
等方法來授權多個Post
動作,方法名不限。
注:如果你在使用 Artisan 命令生成策略類的時候使用了--model
選項,那麼策略類中就會包含了view
、create
、update
和delete
授權動作方法。
不帶模型的方法
有些策略方法只接收當前認證的使用者,並不接收授權的模型例項作為引數,這種用法在授權create
動作的時候很常見。例如,建立一篇部落格的時候,你可能想要檢查檢查當前使用者是否有權建立新部落格。
當定義不接收模型例項的策略方法時,例如create
方法,可以這麼做:
/** * 判斷當前使用者是否可以建立文章. * * @param\App\User$user * @return bool */ public function create(User $user) { // }
訪客使用者
預設情況下,如果傳入的 HTTP 請求由訪客使用者(未登入)發起,所有 Gate 和 Policy 檢查都會返回false
,不過,你可以通過宣告認證使用者引數「可選」或者預設值為null
來允許訪客使用者請求通過授權檢查:
<?php namespace App\Policies; use App\User; use App\Post; class PostPolicy { /** * Determine if the given post can be updated by the user. * * @param\App\User$user * @param\App\Post$post * @return bool */ public function update(?User $user, Post $post) { return $user->id === $post->user_id; } }
策略過濾器
對特定使用者,你可能想要在一個策略方法中對其授權所有許可權,比如後臺管理員。要實現這個功能,需要在 Policy 類中定義一個before
方法,before
方法會在 Policy 類的所有其他方法執行前執行,從而確保在其他策略方法呼叫前執行其中的邏輯:
public function before($user, $ability) { if ($user->isSuperAdmin()) { return true; } }
如果你想要禁止所有授權,可以在before
方法中返回false
。如果返回null
,該授權會落入策略方法。
注:如果 Policy 類沒有包含與待檢查許可權名稱相匹配的授權方法時,該 Policy 類的before
方法將不會被呼叫。
使用 Policy 授權動作
通過 User 模型
Laravel 自帶的User
模型提供了兩個方法用於授權動作:can
和cant
。can
方法接收你想要授權的動作和對應的模型作為引數。例如,下面的例子我們判斷使用者是否被授權更新給定的Post
模型:
if ($user->can('update', $post)) { // }
如果給定模型對應的策略已經註冊,則can
方法會自動呼叫相應的策略並返回布林結果。如果給定模型沒有任何策略被註冊,can
方法將會嘗試呼叫與動作名稱相匹配的 Gate 閉包。
不依賴模型的動作
有些動作比如create
並不需要依賴給定模型例項,在這些場景中,可以傳遞一個類名到can
方法,這個類名會在進行授權的時候用於判斷使用哪一個策略:
use App\Post; if ($user->can('create', Post::class)) { // Executes the "create" method on the relevant policy... }
通過中介軟體
Laravel 提供了一個可以在請求到達路由或控制器之前進行授權的中介軟體 ——Illuminate\Auth\Middleware\Authorize
,預設情況下,這個中介軟體在App\Http\Kernel
類中被分配了一個can
別名,下面我們來探究如何使用can
中介軟體授權使用者更新部落格文章動作:
use App\Post; Route::put('/post/{post}', function (Post $post) { // The current user may update the post... })->middleware('can:update,post');
在這個例子中,我們傳遞了兩個引數給can
中介軟體,第一個是我們想要授權的動作名稱,第二個是我們想要傳遞給策略方法的路由引數。在這個例子中,由於我們使用了隱式模型繫結,Post
模型將會被傳遞給策略方法,如果沒有對使用者進行給定動作的授權,中介軟體將會生成並返回一個狀態碼為403
的 HTTP 響應。
不依賴模型的動作
同樣,對那些不需要傳入模型例項的動作如create
,需要傳遞類名到中介軟體,類名將會在授權動作的時候用於判斷使用哪個策略:
Route::post('/post', function () { // The current user may create posts... })->middleware('can:create,App\Post');
通過控制器輔助函式
除了提供給User
模型的輔助函式,Laravel 還為繼承自App\Http\Controllers\Controller
基類的所有控制器提供了authorize
方法,和can
方法類似,該方法接收你想要授權的動作名稱以及相應模型例項作為引數,如果動作沒有被授權,authorize
方法將會丟擲Illuminate\Auth\Access\AuthorizationException
,Laravel 預設異常處理器將會將其轉化為狀態碼為403
的 HTTP 響應:
<?php namespace App\Http\Controllers; use App\Post; use Illuminate\Http\Request; use App\Http\Controllers\Controller; class PostController extends Controller { /** * 更新給定部落格文章. * * @paramRequest$request * @paramPost$post * @return Response * @translator laravelacademy.org */ public function update(Request $request, Post $post) { $this->authorize('update', $post); // The current user can update the blog post... } }
不依賴模型的動作
和之前討論的一樣,類似create
這樣的動作不需要傳入模型例項引數,在這些場景中,可以傳遞類名給authorize
方法,該類名將會在授權動作時判斷使用哪個策略:
/** * 建立一篇新的部落格文章. * * @paramRequest$request * @return Response */ public function create(Request $request) { $this->authorize('create', Post::class); // The current user can create blog posts... }
授權資源控制器
如果你在使用資源控制器,可以利用控制器建構函式中的authorizeResource
方法,該方法會新增相應的can
中介軟體定義到資源控制器方法。
authorizeResource
方法接收模型類名作為第一個引數,以及包含模型 ID 的路由/請求引數名作為第二個引數:
<?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
選項的make:policy
命令來快速生成給定模型類對應的策略類:php artisan make:policy PostPolicy --model=Post
。
通過 Blade 模板
編寫 Blade 模板的時候,你可能想要在使用者被授權特定動作的情況下才顯示對應的檢視模板部分,例如,你可能想要在使用者被授權更新許可權的情況下才顯示更新表單。在這種情況下,你可以使用@can
和@cannot
指令:
@can('update', $post) <!-- 當前使用者可以更新文章 --> @elsecan('create', App\Post::class) <!-- 當前使用者可以建立新文章 --> @endcan @cannot('update', $post) <!-- 當前使用者不能更新文章 --> @elsecannot('create', App\Post::class) <!-- 當前使用者不能建立新文章 --> @endcannot
這種寫法可看作是@if
和@unless
語句的縮寫,上面的@can
和@cannot
語句與下面的語句等價:
@if (Auth::user()->can('update', $post)) <!-- 當前使用者可以更新文章 --> @endif @unless (Auth::user()->can('update', $post)) <!-- 當前使用者不能更新文章 --> @endunless
不依賴模型的動作
和其它授權方法一樣,如果授權動作不需要傳入模型例項的情況下可以傳遞類名給@can
和@cannot
指令:
@can('create', App\Post::class) <!-- 當前使用者可以建立文章 --> @endcan @cannot('create', App\Post::class) <!-- 當前使用者不能建立文章 --> @endcannot