Giới thiệu Authorization trong Laravel:
Laravel cũng cung cấp cách đơn giản để authorize (ủy quyền – phân quyền) user đối với các tài nguyên cho trước. Laravel sử dụng 2 cách chính để phân quyền: gates và policies. Gates và policies giống như routes và controllers. Gates cung cấp cách đơn giản dựa vào closure để phân quyền trong khi policies giống như controllers, nhóm các logic xung quanh 1 model hoặc tài nguyên cụ thể.
Bạn không nhất thiết phải chọn chỉ gates hay policies. Một số ứng dụng phải sử dụng cả 2 và vẫn ổn. Gates là cách tốt nhất cho các action không liên quan đến model hay tài nguyên khác, ví dụ xem 1 dashboard của admin. Ngược lại, policies được sử dụng khi bạn cần phân quyền 1 hành động cho 1 model hoặc tài nguyên cụ thể.
Gates:
Gates là closure xác định 1 user được phân quyền để thực hiện 1 hành động cho trước hay không. Thông thường gate được định nghĩa trong boot method của AuthServiceProvider sử dụng Gate facade. Gates luôn nhận 1 user làm tham số thứ nhất và có thể nhận tham số bổ sung chẳng hạn Eloquent liên quan.
Trong ví dụ này chúng ta định nghĩa 1 gate để xác định user có thể update 1 Post model hay không. Gates thực hiện bằng cách so sánh user id với user_id của post:
use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
/**
* Register any authentication / authorization services.
*/
public function boot(): void
{
Gate::define('update-post', function (User $user, Post $post) {
return $user->id === $post->user_id;
});
}Tương tự controllers, gates có thể được định nghĩa sử dụng class callback array:
use App\Policies\PostPolicy;
use Illuminate\Support\Facades\Gate;
/**
* Register any authentication / authorization services.
*/
public function boot(): void
{
Gate::define('update-post', [PostPolicy::class, 'update']);
}Phân quyền actions:
Để phân quyền action sử dụng gates, sử dụng method allows hoặc denies cung cấp bởi Gate facade. Lưu ý rằng chúng ta không yêu cầu truyền user đã xác thực hiện tại vào những method này. Laravel sẽ tự động truyền user này vào gate closure. Thông thường gọi method phân quyền trong controllers trước khi thực thi 1 action:
<?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');
}
}Nếu bạn muốn xác minh 1 user khác (không phải là user đã đăng nhập) có được quyền hay không, sử dụng 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...
}Ném ngoại lệ phân quyền:
Nếu bạn muốn ném 1 ngoại lệ Illuminate\Auth\Access\AuthorizationException, sử dụng authorize method. Instance của AuthorizationException sẽ tự động convert thành 403 HTTP:
Gate::authorize('update-post', $post);Cung cấp ngữ cảnh bổ sung:
Các method gate (allows, denies, check, any, none, authorize, can, cannot) và authorize blade (@can, @cannot, @canany) có thể nhận 1 array làm tham số thứ 2. Array này được pass dưới dạng tham số cho gate closure, sử dụng cho ngữ cảnh bổ sung khi phân quyền:
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...
}Gate Responses:
Có thể return 1 Illuminate\Auth\Access\Response từ gate:
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.');
});Thậm chí khi bạn trả về 1 response từ gate, Gate::allows vẫn trả về 1 boolean, tuy nhiên bạn có thể sử dụng Gate::inspectđể nhận response đầy đủ từ gate:
$response = Gate::inspect('edit-settings');
if ($response->allowed()) {
// The action is authorized...
} else {
echo $response->message();
}Khi sử dụng method Gate::authorize, cái mà ném ra 1 ngoại lệ AuthorizationException, thông báo lỗi cung cấp bởi response sẽ được truyền tới HTTP response:
Gate::authorize('edit-settings');
// The action is authorized...Chặn gate check:
Thi thoảng bạn muốn cấp tất cả quyền cho 1 user cụ thể. Sử dụng method before để define 1 closure sẽ run trước khi các authorization khác check:
use App\Models\User;
use Illuminate\Support\Facades\Gate;
Gate::before(function (User $user, string $ability) {
if ($user->isAdministrator()) {
return true;
}
});Nếu closure before trả về 1 kết quả khác null thì đó sẽ được coi là kết quả của authorization check.
Bạn có thể dùng after để define 1 closure sẽ thực thi sau khi check authorization:
use App\Models\User;
Gate::after(function (User $user, string $ability, bool|null $result, mixed $arguments) {
if ($user->isAdministrator()) {
return true;
}
});Tương tự before, nếu closure after trả về 1 kết quả khác null thì nó sẽ coi là kết quả của authorization check.
Inline Authorization:
Thỉnh thoảng bạn muốn xác định user hiện tại có quyền thực thi action hay không, mà không cần viết gate cụ thể. Laravel cho phép bạn viết “inline” thông qua method Gate::allowIf và Gate::denyIf:
use App\Models\User;
use Illuminate\Support\Facades\Gate;
Gate::allowIf(fn (User $user) => $user->isAdministrator());
Gate::denyIf(fn (User $user) => $user->banned());Nếu action không được phân quyền hoặc user hiện tại chưa đăng nhập, Laravel sẽ ném ra 1 ngoại lệ Illuminate\Auth\Access\AuthorizationException. Instance của AuthorizationException sẽ tự động chuyển thành 403 HTTP.
Tạo Policies:
Policies là những class mà tổ chức logic phân quyền cho 1 model hoặc tài nguyên cụ thể. Ví dụ, ứng dụng của bạn là 1 blog, bạn có model Post và 1 App\Policies\PostPolicy tương ứng để phân quyền user action như create hoặc update post.
Bạn có thể khởi tạo policy sử dụng make:policy. Policy sẽ được tạo ra ở thư mục app/Policies:
php artisan make:policy PostPolicyNếu bạn muốn tạo 1 class với method mẫu liên quan đến view, create, update, delete, sử dụng option --model:
php artisan make:policy PostPolicy --model=PostĐăng ký Policies: là cách bạn thông báo cho Laravel policies nào sẽ được sử dụng khi phân quyền các action với 1 model nào đó. Trong AuthServiceProvider có 1 thuộc tính policies để map Eloquent với policies tương ứng:
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
Post::class => PostPolicy::class,
];
/**
* Register any application authentication / authorization services.
*/
public function boot(): void
{
// ...
}
}Policy Auto-Discovery:
Thay vì đăng ký policies bằng tay, Laravel có thể tự động tìm thấy policies miễn là policy và model tuân thủ quy ước đặt tên của Laravel. Cụ thể, policies phải trong thư mục Policies ở trong hoặc trên thư mục chứa models. Ví dụ model đặt ở app/Models trong khi policies phải đặt ở app/Policies. Trong tình huống này, Laravel sẽ check policies ở trong app/Models/Policies sau đó app/Policies. Thêm nữa tên policy phải khớp với tên model và có hậu tố Policy. Một model User sẽ tương ứng với UserPolicy.
Viết policies:
Policy methods:
Ví dụ define method update để xác định user nào được phép update Post model đã cho. Method update sẽ nhận 1 User và Post làm tham số, trả về true hoặc false. Ở đây ta sẽ xác minh user id có khớp với user_id của post không:
<?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;
}
}Bạn có thể define thêm các method khác như view, delete …
Tất cả policies được resolve qua service container, cho phép bạn type-hint bất kỳ phụ thuộc cần thiết nào trong constructor của policy class.
Policy Response:
Chúng ta cũng có thể trả về 1 Illuminate\Auth\Access\Response từ policy:
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.');
}Khi trả về 1 authorization response từ policy, Gate::allows vẫn trả về 1 boolean, bạn có thể dùng Gate::inspect để lấy full response trả về bởi gate:
use Illuminate\Support\Facades\Gate;
$response = Gate::inspect('update', $post);
if ($response->allowed()) {
// The action is authorized...
} else {
echo $response->message();
}Khi sử dụng Gate::authorize, cái mà ném ra 1 ngoại lệ AuthorizationException nếu action không được ủy quyền, thông báo lỗi cung cấp bởi authorization response sẽ được truyền tới HTTP response:
Gate::authorize('update', $post);
// The action is authorized...Methods without models:
Một số method của policy chỉ nhận instance của user hiện tại. Tình huống này phổ biến khi authorize action create. Ví dụ, bạn tạo 1 blog, bạn chỉ cần xác minh user được quyền tạo post:
/**
* Determine if the given user can create posts.
*/
public function create(User $user): bool
{
return $user->role == 'writer';
}Guest users:
Mặc định tất cả gates và policies đều trả về false nếu user chưa đăng nhập. Tuy nhiên bạn có thể cho phép những authorization checks đi qua gates và policies bằng cách khai báo kiểu type-hint tùy chọn, hoặc cung cấp giá trị null cho tham số user:
<?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;
}
}Policy filters:
Đối với user cụ thể, bạn có thể cần authorize tất cả actions với policy cho trước. Dùng method before trong policy, method này sẽ thực hiện trước tất cả method khác. Tính năng này thường xuyên được sử dụng để authorize admin:
use App\Models\User;
/**
* Perform pre-authorization checks.
*/
public function before(User $user, string $ability): bool|null
{
if ($user->isAdministrator()) {
return true;
}
return null;
}Bạn có thể từ chối tất cả authorize checks khi return false trong method này. Nếu return null, authorize check sẽ được chuyển cho policy method.
Authorize action sử dụng Policies:
Thông qua User Model:
App\Models\User có 2 method can và cannot. 2 method này nhận tên action bạn cần authorize và model liên quan:
<?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');
}
}Nếu 1 policy đăng ký cho model cho trước, method can sẽ tự động gọi policy phù hợp và trả về boolean. Nếu không có policy nào đăng ký cho model, method can sẽ gọi closure-based Gate khớp với action.
Actions không yêu cầu models:
Một số action có thể tương ứng với policy method như create mà không yêu cầu model. Trong tình huống này bạn có thể pass 1 class name vào method can. Tên class sẽ được sử dụng xác định policy nào sẽ được dùng:
<?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');
}
}Via controller Helpers:
Laravel cung cấp method authorize cho controllers tương tự can method. Method này chấp nhận tên action và model liên quan. Nếu action không được authorize, method authorize sẽ ném ra ngoại lệ Illuminate\Auth\Access\AuthorizationException được convert thành HTTP 403:
<?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 blog post.
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(Request $request, Post $post): RedirectResponse
{
$this->authorize('update', $post);
// The current user can update the blog post...
return redirect('/posts');
}
}Action không yêu cầu models:
Như đã nói, 1 số policy methods như create không cần model. Trong trường hợp này, bạn nên truyền 1 class name vào method authorize. Tên class này để xác định policy nào sẽ được sử dụng:
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
/**
* Create a new blog post.
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function create(Request $request): RedirectResponse
{
$this->authorize('create', Post::class);
// The current user can create blog posts...
return redirect('/posts');
}Authorize resource controllers:
Nếu bạn sử dụng resource controllers, bạn có thể sử dụng method authorizeResource trong constructor. Method này sẽ gán can middleware phù hợp vào method của controller.
Method authorizeResource chấp nhận tên class model làm tham số thứ 1, tên tham số router / request chứa model ID làm tham số thứ 2. Bạn cần chắc chắn rằng resource controller của bạn được tạo bằng lệnh --model:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Post;
class PostController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Post::class, 'post');
}
}Những method sau sẽ map với policy tương ứng. Khi request đến những controller method này, policy tương ứng sẽ được gọi trước khi controller được thực thi:
| Controller Method | Policy Method |
| index | viewAny |
| show | view |
| create | create |
| store | create |
| edit | update |
| update | update |
| destroy | delete |
Bạn có thể dùng lệnh make:policy với tùy chọn --model để tạo nhanh policy class cho model:
php artisan make:policy PostPolicy --model=PostThông qua middleware:
Mặc định middleware Illuminate\Auth\Middleware\Authorize được gán can trong Kernel class. Xem ví dụ sau sử dụng middleware can:
use App\Models\Post;
Route::put('/post/{post}', function (Post $post) {
// The current user may update the post...
})->middleware('can:update,post');Ở ví dụ trên ta pass cho method can 2 tham số, 1 là tên của action, 2 là tên của route. Trong trường hợp này nhờ sử dụng bind model ngầm định, model Post được pass cho policy method.
Để thuận tiện ta có thể gán middleware can cho route sử dụng method can:
use App\Models\Post;
Route::put('/post/{post}', function (Post $post) {
// The current user may update the post...
})->can('update', 'post');Action không yêu cầu models:
Một lần nữa, với những policy methods như create không yêu cầu model, trong trường hợp này bạn cần pass 1 class name đến method authorize. Tên class này sử dụng để xác định policy nào sẽ được sử dụng:
Route::post('/post', function () {
// The current user may create posts...
})->middleware('can:create,App\Models\Post');Chỉ định toàn bộ tên class trong middleware có thể cồng kềnh, bạn có thể dùng can middleware cho route:
use App\Models\Post;
Route::post('/post', function () {
// The current user may create posts...
})->can('create', Post::class);Thông qua blade templates:
Dùng chỉ thị @can và @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... -->
@endcannotChỉ thị @can và @cannot ở trên tương đương với các câu lệnh sau:
@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... -->
@endunlessSử dụng @canany để xác minh user có được thực hiện bất kỳ action nào từ array cho trước không:
@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... -->
@endcananyActions không yêu cầu models:
Tương tự các method authorization khác, bạn có thể pass class name cho chỉ thị @can và @cannot nếu action không yêu cầu model:
@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... -->
@endcannotHỗ trợ ngữ cảnh bổ sung:
Khi phân quyền sử dụng policies, bạn có thể pass 1 array như 1 tham số thứ 2 tới các hàm authorization và helper. Giá trị đầu tiên trong array được sử dụng để xác định policy nào sẽ được gọi, những giá trị còn lại được pass như tham số đến policy method và sử dụng làm ngữ cảnh bổ sung. Ví dụ PostPolicy chứa tham số bổ sung $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);
}Chúng ta có thể gọi policy như sau:
/**
* Update the given blog post.
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(Request $request, Post $post): RedirectResponse
{
$this->authorize('update', [$post, $request->category]);
// The current user can update the blog post...
return redirect('/posts');
}