Toidicode.com

Toidicode.com

BASIC TO ADVANCE

Bài 22: Phân quyền với Gate và Policy trong Laravel 13

Ở bài trước, Bình vẫn mở được project của An khi gõ thẳng URL. Tiếp tục với series, bài này mình sẽ giới thiệu với mọi người về phân quyền với Gate và Policy trong Laravel 13, rồi làm chức năng sửa, xóa project chỉ dành cho chủ project.

1. Phân quyền và Gate.

Xác thực trả lời "bạn là ai", còn phân quyền (authorization) trả lời "bạn được làm gì". Laravel có hai cách: Gate là closure kiểm tra một quyền, hợp với quyền không gắn với model (VD vào trang quản trị); Policy là class gom các quyền của một model.

Gate khai báo trong boot() của AppServiceProvider, tham số đầu luôn là user đang đăng nhập:

- app/Providers/AppServiceProvider.php (trích, thêm use App\Models\Project;, use App\Models\User;, use Illuminate\Support\Facades\Gate;)

// chỉ chủ project mới được sửa project
Gate::define('update-project', function (User $user, Project $project) {
    return $user->id === $project->user_id;
});

Kiểm tra bằng Gate::allows() (được phép thì true), Gate::denies() (ngược lại) hoặc Gate::authorize() (không được phép thì ném AuthorizationException, Laravel đổi thành lỗi 403). Thử trong php artisan tinker:

$project = Project::find(1); // project của An (user 1)
Gate::allows('update-project', $project); // false, chưa đăng nhập
Auth::loginUsingId(1);
Gate::allows('update-project', $project); // true
Gate::forUser(User::find(2))->denies('update-project', $project); // true, Bình không được sửa
Auth::loginUsingId(2);
Gate::authorize('update-project', $project); // AuthorizationException  This action is unauthorized.

Project còn quyền xem, xóa..., mỗi quyền một gate sẽ rối, nên mình gom vào policy và xóa gate update-project này đi.

2. Tạo Policy cho Project.

php artisan make:policy ProjectPolicy --model=Project
# INFO  Policy [app/Policies/ProjectPolicy.php] created successfully.

Có --model, file sinh sẵn các method viewAny, view, create, update, delete, restore, forceDelete, tất cả trả về false. Project không dùng soft delete nên mình bỏ hai method cuối:

- app/Policies/ProjectPolicy.php

<?php

namespace App\Policies;

use App\Models\Project;
use App\Models\User;

class ProjectPolicy
{
    public function viewAny(User $user): bool
    {
        return true;
    }

    // chỉ chủ project mới xem, sửa, xóa được project
    public function view(User $user, Project $project): bool
    {
        return $user->id === $project->user_id;
    }

    public function create(User $user): bool
    {
        return true;
    }

    public function update(User $user, Project $project): bool
    {
        return $user->id === $project->user_id;
    }

    public function delete(User $user, Project $project): bool
    {
        return $user->id === $project->user_id;
    }
}

Policy không cần đăng ký: với model App\Models\Project, Laravel tự tìm App\Policies\ProjectPolicy (tên model + Policy, trong thư mục Policies). Đặt tên khác thì đăng ký bằng Gate::policy(Project::class, ...) hoặc attribute #[UsePolicy] trên model.

Model User có sẵn can(), cannot(): $user->can('update', $project) tự gọi ProjectPolicy::update(), quyền không cần model như create thì truyền tên class Project::class.

File đính kèm thuộc về task, nên mình tạo thêm make:policy TaskPolicy --model=Task và chỉ giữ hai method:

- app/Policies/TaskPolicy.php (trích, phần thân class)

// chủ project hoặc người được giao task mới xem, tải file của task
public function view(User $user, Task $task): bool
{
    return $user->id === $task->project->user_id || $user->id === $task->assignee_id;
}

// chỉ chủ project mới tải lên, xóa file của task
public function update(User $user, Task $task): bool
{
    return $user->id === $task->project->user_id;
}

3. Kiểm tra quyền trong controller và route.

Laravel 13 có attribute #[Authorize] gắn middleware can cho method của controller (giống #[Middleware] ở Bài 9). Method show() của ProjectController thêm #[Authorize('view', 'project')], ba method còn trống từ Bài 5 viết như sau:

- app/Http/Controllers/ProjectController.php (trích, thêm use Illuminate\Routing\Attributes\Controllers\Authorize; và use App\Http\Requests\UpdateProjectRequest;)

#[Authorize('update', 'project')]
public function edit(Project $project): View
{
    return view('projects.edit', compact('project'));
}

public function update(UpdateProjectRequest $request, Project $project): RedirectResponse
{
    // UpdateProjectRequest đã kiểm tra quyền update và validate
    $project->update($request->validated());

    return to_route('projects.show', $project)->with('status', 'Đã cập nhật project.');
}

#[Authorize('delete', 'project')]
public function destroy(Project $project): RedirectResponse
{
    // tasks và attachments bị xóa theo nhờ khóa ngoại cascadeOnDelete
    $project->delete();

    return to_route('projects.index')
        ->with('status', 'Đã xóa project "'.$project->name.'".');
}

Trong đó:

  • 'view', 'update', 'delete': tên method trong policy.
  • 'project': tên tham số route {project}, middleware lấy model đã route model binding truyền vào policy. Đặt attribute trên class thì thêm only, except như #[Middleware].

AttachmentController cũng vậy (thêm use Illuminate\Routing\Attributes\Controllers\Authorize; và use Illuminate\Support\Facades\Gate;): index có #[Authorize('view', 'task')], store có #[Authorize('update', 'task')]. Route /attachments/{attachment} không có {task}, nên dòng đầu của show() là Gate::authorize('view', $attachment->task);, của destroy() là Gate::authorize('update', $attachment->task);. Gate thấy tham số là Task nên hỏi TaskPolicy.

Chú ý: Thiếu dòng use của Authorize thì PHP hiểu attribute là App\Http\Controllers\Authorize và bỏ qua mà không báo lỗi gì, route sẽ không được kiểm tra quyền. Luôn đăng nhập một tài khoản khác để thử lại, VD Bình mở /tasks/1/attachments của An phải nhận 403.

Code cũ hay viết $this->authorize(), nhưng từ Laravel 11 nó cần thêm trait AuthorizesRequests vào class Controller gốc, nên series dùng Gate::authorize().

Middleware can cũng gắn thẳng vào route, ->can('view', 'project') là cách viết ngắn của ->middleware('can:view,project'):

- routes/web.php (trích, trong group projects)

Route::get('/projects/{project}/tasks', ListProjectTasks::class)
    ->name('projects.tasks.index')
    // middleware can: gọi ProjectPolicy::view() với project của route
    ->can('view', 'project');

4. Form Request, @can và trang 403.

Method authorize() của StoreProjectRequest (Bài 10) đổi return true; thành return $this->user()->can('create', Project::class); (nhớ use App\Models\Project;). Form sửa dùng chung rule nên php artisan make:request UpdateProjectRequest rồi cho kế thừa StoreProjectRequest, chỉ đổi authorize(). Trả về false thì nhận 403 trước cả khi validate:

- app/Http/Requests/UpdateProjectRequest.php

<?php

namespace App\Http\Requests;

class UpdateProjectRequest extends StoreProjectRequest
{
    public function authorize(): bool
    {
        // project lấy từ tham số {project} của route
        return $this->user()->can('update', $this->route('project'));
    }
}

View projects/edit.blade.php chép form tạo project, đổi action sang route('projects.update', $project), thêm @method('PUT') và old('name', $project->name). Blade có @can, @cannot để ẩn nút người dùng không có quyền:

- resources/views/projects/show.blade.php (trích, dưới mô tả)

<div class="mb-4 flex gap-4">
    @can('update', $project)
        <a href="{{ route('projects.edit', $project) }}" class="text-blue-600">Sửa</a>
    @endcan

    @can('delete', $project)
        <form method="POST" action="{{ route('projects.destroy', $project) }}">
            @csrf
            @method('DELETE')
            <button type="submit" class="text-red-600">Xóa project</button>
        </form>
    @endcan
</div>

Trang file của task cũng bọc form tải lên và nút xóa trong @can('update', $task). Trang lỗi thì làm như trang 404 ở Bài 11: chép thành errors/403.blade.php, đổi số 403 và in {{ __($exception->getMessage() ?: 'This action is unauthorized.') }}. Thêm dòng "This action is unauthorized.": "Bạn không có quyền thực hiện thao tác này." vào lang/vi.json để dịch message mặc định.

Chạy php artisan migrate:fresh --seed rồi đăng nhập [email protected]: mở /projects/1, /projects/1/edit hay gửi form sửa, xóa đều nhận 403 "Bạn không có quyền thực hiện thao tác này.", request JSON nhận {"message": "This action is unauthorized."}. Bình được giao task 5 của An nên vẫn mở được /tasks/5/attachments và tải file, nhưng không thấy form tải lên.

Trang /stats vẫn công khai số liệu của mọi người, mình thêm ->middleware('auth') cho route và ->where('projects.user_id', $request->user()->id) vào query của TaskStatsController (thêm tham số Request $request), để mỗi người chỉ thấy thống kê project của mình.

5. Lời kết.

Như vậy chúng ta đã tìm hiểu xong phân quyền với Gate và Policy trong Laravel 13: Gate::define, policy, #[Authorize], middleware can, @can và authorize() của Form Request. Các bạn xem thêm tại tài liệu chính thức. Bài sau mình sẽ giới thiệu về xây dựng API với Sanctum để TaskFlow có API danh sách task bảo vệ bằng token.

Đăng ký nhận tin.

Chúng tôi chỉ gửi tối đa 2 lần trên 1 tháng. Tuyên bố không spam mail!

Vũ Thanh Tài

About author
The best way to learn is to share
Xem tất cả bài đăng

0 Comments

Bài viết chưa có ai bình luận, hãy là người đầu tiên đi bạn!

Bình luận

Captcha