Bảo Mật Laravel: Gate & Policy - Ai Được Làm Gì?
Chào các "coder nhí" và cả những "lão làng" đang "vật lộn" với code! Anh Creyt đây, hôm nay chúng ta sẽ cùng "mổ xẻ" một khái niệm cực kỳ quan trọng trong thế giới lập trình web nói chung và Laravel nói riêng: Authorization – hay còn gọi là Phân quyền. Đừng nhầm lẫn với Authentication (Xác thực) nhé, đó là câu chuyện "Bạn là ai?", còn Authorization là "Bạn được làm gì?". Authorization là gì và tại sao chúng ta cần nó? Tưởng tượng thế này: Bạn có một căn nhà "full nội thất" (ứng dụng web của bạn đó). Authentication giống như việc bạn có chìa khóa để mở cửa chính – nó xác định bạn là chủ nhà, chứ không phải một "kẻ đột nhập". Nhưng một khi đã vào nhà, bạn có được phép "đập phá bức tường", "thay đổi thiết kế nội thất" hay chỉ được "ngồi xem TV" thôi? Đó chính là lúc Authorization "ra tay"! Authorization trong Laravel là cơ chế cho phép chúng ta kiểm soát ai có quyền truy cập vào một tài nguyên nhất định hoặc thực hiện một hành động cụ thể. Nó giống như một "bộ phận an ninh nội bộ" của ứng dụng, đảm bảo rằng chỉ những người dùng có "quyền hạn" phù hợp mới được phép "đụng chạm" vào những thứ "nhạy cảm" hay thực hiện các "tác vụ đặc biệt". Trong Laravel, chúng ta có hai "công cụ" chính để triển khai Authorization một cách "thanh lịch" và "hiệu quả": Gates và Policies. 1. Gates: "Người gác cổng" đa năng Gates (Cổng) là những "người gác cổng" linh hoạt, dùng để định nghĩa các quyền hạn đơn giản, không gắn liền trực tiếp với một model cụ thể. Chúng hoạt động dựa trên các closures (hàm ẩn danh) và rất tiện lợi cho các quyền hạn "chung chung" hoặc khi bạn cần kiểm tra một điều kiện phức tạp mà không muốn tạo hẳn một class riêng. Để làm gì? Kiểm tra quyền truy cập vào một tính năng tổng thể (ví dụ: "có thể quản lý người dùng"). Kiểm tra một điều kiện đặc biệt không liên quan đến một model cụ thể. Cách dùng: Bạn định nghĩa Gates trong file app/Providers/AuthServiceProvider.php, bên trong phương thức boot(). // app/Providers/AuthServiceProvider.php use Illuminate\Support\Facades\Gate; use App\Models\User; use App\Models\Post; class AuthServiceProvider extends ServiceProvider { // ... public function boot() { $this->registerPolicies(); // Định nghĩa một Gate đơn giản: 'edit-settings' // Chỉ cho phép user có role 'admin' chỉnh sửa cài đặt Gate::define('edit-settings', function (User $user) { return $user->role === 'admin'; }); // Định nghĩa một Gate phức tạp hơn: 'update-post' // Cho phép user cập nhật bài viết nếu họ là chủ bài viết đó Gate::define('update-post', function (User $user, Post $post) { return $user->id === $post->user_id; }); } } Sử dụng Gate: Trong Controller: // app/Http/Controllers/SettingsController.php use Illuminate\Support\Facades\Gate; class SettingsController extends Controller { public function edit() { // Cách 1: Sử dụng Gate::allows() if (Gate::allows('edit-settings')) { // Người dùng có quyền chỉnh sửa cài đặt return view('settings.edit'); } // Cách 2: Sử dụng Gate::denies() hoặc abort(403) // Gate::denies('edit-settings') là ngược lại của Gate::allows() // abort(403) sẽ tự động ném ra lỗi 403 Forbidden nếu user không có quyền Gate::authorize('edit-settings'); // Dễ hơn nhiều! return view('settings.edit'); } public function update(Post $post) { // Dùng Gate với tham số model Gate::authorize('update-post', $post); // ... logic cập nhật bài viết ... return redirect()->route('posts.show', $post)->with('success', 'Bài viết đã được cập nhật!'); } } Trong Blade (View): <!-- resources/views/settings/edit.blade.php --> @can('edit-settings') <a href="#" class="btn btn-primary">Chỉnh sửa cài đặt hệ thống</a> @endcan <!-- resources/views/posts/show.blade.php --> @can('update-post', $post) <a href="{{ route('posts.edit', $post) }}" class="btn btn-warning">Sửa bài viết</a> @endcan @cannot('update-post', $post) <p>Bạn không có quyền sửa bài viết này.</p> @endcannot 2. Policies: "Sổ tay quy tắc" cho từng đối tượng Policies (Chính sách) là những "sổ tay quy tắc" chuyên biệt, được thiết kế để quản lý quyền hạn cho một model cụ thể. Nếu Gates là "người gác cổng" tổng quát, thì Policies giống như một "bộ quy tắc nội bộ" chi tiết cho từng loại tài sản (ví dụ: một bộ quy tắc cho Post, một bộ khác cho User, v.v.). Chúng rất phù hợp cho các thao tác CRUD (Create, Read, Update, Delete) trên các tài nguyên. Để làm gì? Kiểm soát quyền truy cập và thao tác trên một model cụ thể (ví dụ: ai có thể xem, tạo, sửa, xóa một Post). Giúp code sạch sẽ, dễ đọc và dễ bảo trì hơn khi bạn có nhiều quyền hạn liên quan đến một model. Cách dùng: Bước 1: Tạo Policy Bạn dùng Artisan để tạo một Policy. Ví dụ, với model Post: php artisan make:policy PostPolicy --model=Post Lệnh này sẽ tạo file app/Policies/PostPolicy.php với các phương thức CRUD cơ bản đã được "nhúng" sẵn. Bước 2: Đăng ký Policy Bạn cần "nói" cho Laravel biết model nào sẽ được quản lý bởi Policy nào. Điều này cũng được thực hiện trong app/Providers/AuthServiceProvider.php: // app/Providers/AuthServiceProvider.php use App\Models\Post; use App\Policies\PostPolicy; class AuthServiceProvider extends ServiceProvider { /** * The model to policy mappings for the application. * * @var array<class-string, class-string> */ protected $policies = [ Post::class => PostPolicy::class, ]; public function boot() { $this->registerPolicies(); // ... các Gate khác nếu có ... } } Bước 3: Viết logic trong Policy Trong PostPolicy.php, bạn sẽ định nghĩa các phương thức tương ứng với các hành động. Mỗi phương thức sẽ nhận User và Post (hoặc chỉ User nếu là hành động create): // app/Policies/PostPolicy.php use App\Models\User; use App\Models\Post; class PostPolicy { /** * Determine whether the user can view any models. */ public function viewAny(User $user): bool { // Mọi người đều có thể xem danh sách bài viết return true; } /** * Determine whether the user can view the model. */ public function view(User $user, Post $post): bool { // Mọi người đều có thể xem một bài viết cụ thể return true; } /** * Determine whether the user can create models. */ public function create(User $user): bool { // Chỉ user đã đăng nhập mới có thể tạo bài viết return (bool) $user->id; } /** * Determine whether the user can update the model. */ public function update(User $user, Post $post): bool { // Chỉ chủ bài viết mới có thể cập nhật return $user->id === $post->user_id; } /** * Determine whether the user can delete the model. */ public function delete(User $user, Post $post): bool { // Chỉ chủ bài viết mới có thể xóa return $user->id === $post->user_id; } /** * Optional: "Super Admin" override (trước khi các phương thức khác được gọi) */ public function before(User $user, string $ability) { if ($user->role === 'super-admin') { return true; // Super admin có thể làm mọi thứ! } } } Sử dụng Policy: Trong Controller: Laravel cung cấp một trait AuthorizesRequests cho các controller, giúp bạn dễ dàng sử dụng Policies. // app/Http/Controllers/PostController.php use App\Models\Post; use Illuminate\Http\Request; class PostController extends Controller { public function __construct() { // Sử dụng middleware 'can' để bảo vệ toàn bộ controller hoặc từng phương thức // 'create' là tên phương thức trong Policy, 'post' là tên tham số route (nếu có) $this->middleware('auth')->except(['index', 'show']); // Đảm bảo user đã đăng nhập $this->middleware('can:create,App\Models\Post')->only('create', 'store'); $this->middleware('can:update,post')->only('edit', 'update'); $this->middleware('can:delete,post')->only('destroy'); } public function index() { $posts = Post::all(); return view('posts.index', compact('posts')); } public function create() { // Không cần gọi $this->authorize('create', Post::class) nếu đã dùng middleware return view('posts.create'); } public function edit(Post $post) { // Laravel sẽ tự động gọi PostPolicy@update và truyền $user, $post vào $this->authorize('update', $post); return view('posts.edit', compact('post')); } public function destroy(Post $post) { $this->authorize('delete', $post); $post->delete(); return redirect()->route('posts.index')->with('success', 'Bài viết đã bị xóa!'); } } Trong Blade (View): Cũng tương tự như Gate, bạn dùng @can với tên phương thức của policy và model: <!-- resources/views/posts/index.blade.php --> @can('create', App\Models\Post::class) <a href="{{ route('posts.create') }}" class="btn btn-success">Tạo bài viết mới</a> @endcan <!-- resources/views/posts/show.blade.php --> @can('update', $post) <a href="{{ route('posts.edit', $post) }}" class="btn btn-warning">Sửa bài viết</a> @endcan @can('delete', $post) <form action="{{ route('posts.destroy', $post) }}" method="POST" style="display:inline;"> @csrf @method('DELETE') <button type="submit" class="btn btn-danger" onclick="return confirm('Bạn có chắc chắn muốn xóa bài viết này?')">Xóa bài viết</button> </form> @endcan Mẹo vặt và Best Practices từ "lão làng" Creyt: Khi nào dùng Gate, khi nào dùng Policy? Policies là "công cụ vàng" khi bạn cần kiểm soát quyền hạn cho một model cụ thể (ví dụ: Post, User, Product). Hãy nghĩ đến các thao tác CRUD. Policies giúp code của bạn gọn gàng, có tổ chức hơn nhiều. Gates là lựa chọn "tiện lợi" cho các quyền hạn không gắn liền với model nào, hoặc các quyền hạn "chung chung" của hệ thống (ví dụ: "có quyền truy cập trang admin", "có thể xem báo cáo tài chính"). Chúng cũng hữu ích khi bạn cần kiểm tra điều kiện rất đơn giản, chỉ cần một hàm closure là đủ. Sử dụng before trong Policy: Nếu bạn có "super admin" (quản trị viên tối cao) có quyền làm mọi thứ, hãy dùng phương thức before trong Policy. Nó sẽ được gọi trước bất kỳ phương thức kiểm tra quyền nào khác, và nếu nó trả về true, quyền sẽ được cấp ngay lập tức mà không cần kiểm tra thêm. Tiết kiệm thời gian xử lý! Hạn chế logic trong Controller: Đừng "nhồi nhét" logic kiểm tra quyền vào Controller. Hãy "đẩy" chúng vào Gates hoặc Policies. Controller của bạn sẽ "thon gọn" và dễ đọc hơn rất nhiều. Middleware can: Đây là "vũ khí bí mật" để bảo vệ các route một cách "thanh lịch". Thay vì gọi Gate::authorize() hay $this->authorize() trong mỗi phương thức controller, hãy dùng middleware('can:ability,model_parameter') ngay trong constructor của controller hoặc trong file web.php. Tên quyền rõ ràng: Đặt tên cho Gates và các phương thức trong Policies thật rõ ràng, dễ hiểu (ví dụ: update-post thay vì up_p). Ứng dụng thực tế: "Đời sống" của Authorization Authorization "hiện diện" khắp mọi nơi, từ những "ông lớn" đến những "startup nhỏ bé": Hệ thống quản lý nội dung (CMS) / Blog (WordPress, Medium, Laravel Forge): Chỉ tác giả mới được sửa bài viết của họ, biên tập viên mới được duyệt và xuất bản, quản trị viên mới được quản lý người dùng và cài đặt hệ thống. Sàn thương mại điện tử (Shopee, Lazada, Tiki): Chỉ người bán mới được thêm, sửa, xóa sản phẩm của họ. Khách hàng chỉ được xem, thêm vào giỏ hàng và đặt mua. Admin có thể quản lý tất cả sản phẩm, đơn hàng, người dùng. Mạng xã hội (Facebook, Twitter): Chỉ bạn mới được xóa bài đăng của mình, sửa thông tin cá nhân. Bạn bè chỉ được xem, bình luận. Hệ thống quản lý dự án (Jira, Trello): Chỉ thành viên trong dự án mới được xem các task. Chỉ người được giao task mới được thay đổi trạng thái task. Chỉ quản lý dự án mới được thêm/xóa thành viên. Bạn thấy đó, Authorization không chỉ là một khái niệm "trừu tượng" mà là một "trụ cột" không thể thiếu trong mọi ứng dụng web hiện đại. Nắm vững nó, bạn không chỉ "nâng tầm" kỹ năng lập trình của mình mà còn "bảo vệ" ứng dụng của mình khỏi những "sai lầm" không đáng có. Chúc các bạn "code" vui vẻ và "bảo mật" thành công! Thuộc Series: Lavarel Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!