Nội dung bài viết
- 1.Phân biệt authentication và authorization
- 2.Authentication trong Laravel
- 2.1Guard và provider
- 2.2Cách nhanh nhất: starter kit
- 2.3Tự viết đăng nhập với Auth::attempt()
- 2.4Lấy người dùng hiện tại và bảo vệ route
- 2.5Xác thực API và SPA: Sanctum
- 3.Authorization trong Laravel: Gate và Policy
- 3.1Gate: quyền đơn giản
- 3.2Policy: quyền theo model
- 4.Kiểm tra quyền ở đâu?
- 4.1Trong controller
- 4.2Trên route bằng middleware can
- 4.3Trong Blade
- 5.Lỗi thường gặp khi làm xác thực và phân quyền
- 6.Kinh nghiệm từ Hozitech
Authentication (xác thực) là bước xác định người dùng là ai, ví dụ kiểm tra email và mật khẩu khi đăng nhập. Authorization (phân quyền) là bước quyết định người dùng đã xác thực đó được làm gì, ví dụ chỉ tác giả mới được sửa bài viết của mình. Trong Laravel, xác thực do guard, provider và middleware auth đảm nhận; phân quyền dùng Gate và Policy.
Bài viết cập nhật theo Laravel 13 (bản hiện hành, PHP 8.3+), đi từ khái niệm tới code đăng nhập, bảo vệ route, viết Gate, Policy và cách kiểm tra quyền trong controller, route, Blade.

Phân biệt authentication và authorization
Hãy hình dung một tòa nhà văn phòng. Quầy lễ tân kiểm tra thẻ nhân viên để biết bạn là ai: đó là xác thực. Thẻ của bạn mở được tầng nào, phòng nào: đó là phân quyền. Hai bước luôn đi theo thứ tự: chưa biết người dùng là ai thì không thể quyết định họ được làm gì.
- Authentication thất bại: người dùng chưa đăng nhập bị chuyển về trang đăng nhập (với request web) hoặc nhận mã 401 (với API).
- Authorization thất bại: người dùng đã đăng nhập nhưng không có quyền, nhận mã 403 Forbidden.
Authentication trong Laravel
Guard và provider
Hệ thống xác thực của Laravel xoay quanh hai khái niệm, cấu hình trong config/auth.php:
- Guard quyết định cách nhận diện người dùng ở mỗi request. Guard mặc định
webdùng driversession: sau khi đăng nhập, thông tin người dùng nằm trong session, trình duyệt giữ cookie chứa session ID. - Provider quyết định cách lấy người dùng từ nơi lưu trữ. Mặc định là Eloquent với model
App\Models\User.
Driver token trong các bài hướng dẫn cũ đã không còn trong cấu hình mặc định. Với API, Laravel khuyên dùng Sanctum (hoặc Passport nếu thật sự cần đầy đủ OAuth2).
Cách nhanh nhất: starter kit
Khi tạo dự án bằng laravel new, trình cài đặt hỏi bạn chọn starter kit. Laravel 13 có bốn bộ chính thức: React, Vue, Svelte (ba bộ này dùng Inertia) và Livewire. Tất cả dùng Laravel Fortify để có sẵn đăng nhập, đăng ký, quên mật khẩu, xác minh email và xác thực hai lớp. Breeze và Jetstream là các bộ khởi tạo của thế hệ trước, bạn vẫn gặp trong dự án cũ.
composer global require laravel/installer
laravel new my-app
Kể cả khi không dùng starter kit cho sản phẩm thật, cài thử một bộ để đọc code controller, route, view của nó là cách học xác thực rất hiệu quả.
Tự viết đăng nhập với Auth::attempt()
Nếu muốn tự làm, dùng facade Auth. attempt() tìm người dùng theo email, so mật khẩu với bản đã băm (bạn không tự băm mật khẩu nhập vào). Đăng nhập thành công phải tạo lại session để chống tấn công session fixation:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
public function authenticate(Request $request): RedirectResponse
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
if (Auth::attempt($credentials)) {
$request->session()->regenerate();
return redirect()->intended('dashboard');
}
return back()->withErrors([
'email' => 'Email hoặc mật khẩu không đúng.',
])->onlyInput('email');
}
public function logout(Request $request): RedirectResponse
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}
Khi đăng xuất, ngoài Auth::logout() nên hủy session và tạo lại CSRF token. Lý do được giải thích trong bài CSRF là gì và cách chống CSRF trong Laravel.
Lấy người dùng hiện tại và bảo vệ route
use Illuminate\Support\Facades\Auth;
$user = Auth::user(); // model User hoặc null
$id = Auth::id();
if (Auth::check()) {
// đã đăng nhập
}
// Trong controller: $request->user()
Thay vì kiểm tra thủ công trong từng hàm, gắn middleware auth cho route hoặc nhóm route. Người chưa đăng nhập sẽ bị chuyển tới route có tên login:
use App\Http\Controllers\DashboardController;
use Illuminate\Support\Facades\Route;
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
});
Muốn đổi trang chuyển hướng, gọi $middleware->redirectGuestsTo('/dang-nhap') trong bootstrap/app.php. Cách đăng ký và sắp thứ tự middleware được giải thích trong bài middleware trong Laravel.
Xác thực API và SPA: Sanctum
Laravel Sanctum là gói được khuyến nghị cho SPA tách riêng, ứng dụng mobile và API đơn giản. Sanctum kiểm tra cookie session trước (cho SPA cùng domain gốc), nếu không có thì tìm API token trong request. Passport chỉ nên chọn khi cần đầy đủ các luồng OAuth2.
Authorization trong Laravel: Gate và Policy
Laravel có hai cách phân quyền. Có thể ví Gate như route dạng closure và Policy như controller: Gate hợp với quyền không gắn với model (ví dụ "xem trang quản trị"), Policy gom các quyền xoay quanh một model (xem, sửa, xóa bài viết). Một ứng dụng dùng cả hai là bình thường.
Gate: quyền đơn giản
Gate khai báo trong phương thức boot() của App\Providers\AppServiceProvider. Tham số đầu luôn là người dùng hiện tại:
use App\Models\User;
use Illuminate\Support\Facades\Gate;
public function boot(): void
{
Gate::define('view-admin', function (User $user) {
return $user->is_admin;
});
}
use Illuminate\Support\Facades\Gate;
if (Gate::denies('view-admin')) {
abort(403);
}
// hoặc ném AuthorizationException (Laravel tự đổi thành 403)
Gate::authorize('view-admin');
Policy: quyền theo model
php artisan make:policy PostPolicy --model=Post
Với tùy chọn --model, lớp sinh ra có sẵn các phương thức viewAny, view, create, update, delete, restore, forceDelete. Laravel tự tìm Policy nếu đặt đúng quy ước: model App\Models\Post ứng với App\Policies\PostPolicy, không cần đăng ký thủ công.
<?php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
// Quản trị viên được làm mọi thứ; trả về null để đi tiếp vào phương thức bên dưới
public function before(User $user, string $ability): ?bool
{
return $user->is_admin? true : null;
}
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
public function delete(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
}
Model Post ở đây cần cột user_id và quan hệ với User; xem cách khai báo trong bài model trong Laravel.
Kiểm tra quyền ở đâu?
Trong controller
Từ Laravel 11, lớp Controller cơ sở trong dự án mới là lớp rỗng, không còn trait AuthorizesRequests, nên $this->authorize() không dùng được nếu bạn không tự thêm trait. Cách chuẩn hiện nay là Gate::authorize():
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
public function update(Request $request, Post $post): RedirectResponse
{
Gate::authorize('update', $post);
$post->update($request->validate([
'title' => ['required', 'max:255'],
'body' => ['required'],
]));
return redirect()->route('posts.show', $post);
}
Hoặc dùng trực tiếp trên model người dùng: if ($request->user()->cannot('update', $post)) { abort(403); }.
Trên route bằng middleware can
Kiểm tra quyền trước khi request vào controller. Lưu ý tham số route phải là {post} để route model binding đưa model vào Policy:
use App\Http\Controllers\PostController;
use App\Models\Post;
Route::put('/posts/{post}', [PostController::class, 'update'])
->middleware('auth')
->can('update', 'post');
// Hành động không cần model cụ thể: truyền tên class
Route::post('/posts', [PostController::class, 'store'])
->middleware('auth')
->can('create', Post::class);
Ở Laravel 13, có thể khai báo ngay trên phương thức controller bằng attribute #[Authorize('update', 'post')] (namespace Illuminate\Routing\Attributes\Controllers\Authorize). Bài route trong Laravel giải thích cách tham số {post} được đổi thành model.
Trong Blade
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">Sửa bài</a>
@endcan
@cannot('delete', $post)
<p>Bạn không thể xóa bài này.</p>
@endcannot
Ẩn nút trên giao diện chỉ để trải nghiệm tốt hơn, không thay được kiểm tra ở server: người dùng vẫn có thể gửi request trực tiếp, nên route hoặc controller phải kiểm tra lại.
Lỗi thường gặp khi làm xác thực và phân quyền
- Gọi
$this->authorize()trong route closure:$thistrong closure không phải controller, code lỗi. DùngGate::authorize()hoặc middlewarecan. - Tham số route là
{id}nhưng middleware ghican:update,post: tên tham số phải khớp thì model mới được bind và truyền vào Policy. - Quên tạo lại session sau đăng nhập, mở đường cho session fixation.
- Chỉ ẩn nút trên giao diện mà không kiểm tra quyền ở server.
- Kiểm tra vai trò rải rác kiểu
if ($user->role == 'admin')khắp controller thay vì gom vào Policy, khó bảo trì khi thêm vai trò mới.
Kinh nghiệm từ Hozitech
Trong các website và hệ thống quản trị Hozitech xây bằng Laravel, phần quản trị thường có nhiều vai trò (quản trị viên, biên tập viên, nhân viên bán hàng). Cách tổ chức dễ bảo trì nhất là: middleware auth bảo vệ cả nhóm route quản trị, mỗi model có một Policy, Gate chỉ dùng cho quyền tổng quát như truy cập trang cài đặt. Khi khách hàng muốn thêm vai trò mới, chỉ phải sửa Policy chứ không lần từng controller.
Nếu doanh nghiệp bạn cần một website có khu quản trị phân quyền rõ ràng, có thể tham khảo dịch vụ thiết kế website của Hozitech.
Về tác giả
Founder
Xin chào mình là Hiền hiện đang là Founder của Hozitech. Mình là kỹ sư phần mềm có hơn 9+ năm kinh nghiệm trong lĩnh vực thiết kế website, ứng dụng di động... Gần đây mình đang nghiên cứu thêm về lĩnh vực marketing. Mình rất vui khi được chia sẽ những kiến thức và kinh nghiệm của mình đến mọi người!
Câu hỏi thường gặp