Ở các bài trước, chúng ta đã cài đặt Laravel Breeze, đăng ký tài khoản và đăng nhập vào hệ thống.
Tuy nhiên, hiện tại nếu người dùng biết đường dẫn trực tiếp như:
/posts/create
hoặc:
/dashboard
thì liệu họ có thể truy cập được hay không?
Đó chính là nhiệm vụ của Middleware.
Middleware đóng vai trò như một lớp bảo vệ, kiểm tra mọi yêu cầu trước khi cho phép truy cập vào Controller hoặc Route.
Middleware là gì?
Middleware là đoạn mã nằm giữa Request và Controller.
Mọi yêu cầu gửi đến Laravel đều có thể đi qua một hoặc nhiều Middleware.
Sơ đồ:
Người dùng
│
▼
HTTP Request
│
▼
Middleware
│
▼
Controller
│
▼
Response
Nếu Middleware từ chối yêu cầu, Controller sẽ không được thực thi.
Ví dụ thực tế
Giả sử Blog có các chức năng:
Xem bài viết.
Thêm bài viết.
Chỉnh sửa bài viết.
Xóa bài viết.
Khách chưa đăng nhập:
✅ Được xem bài viết.
❌ Không được tạo bài viết.
❌ Không được sửa bài viết.
❌ Không được xóa bài viết.
Middleware sẽ đảm nhận việc kiểm tra này.
Middleware auth
Laravel đã có sẵn Middleware:
auth
Nó sẽ kiểm tra:
Người dùng đã đăng nhập?
│
Có Không
│ │
▼ ▼
Cho phép Chuyển Login
Bảo vệ Route
Ví dụ:
use App\Http\Controllers\PostController;
Route::middleware('auth')->group(function () {
Route::resource(
'posts',
PostController::class
);
});
Toàn bộ các Route của PostController đều yêu cầu đăng nhập.
Chỉ bảo vệ một Route
Ví dụ:
Route::get(
'/dashboard',
function () {
return view('dashboard');
}
)->middleware('auth');
Nếu chưa đăng nhập, Laravel sẽ chuyển người dùng đến trang Login.
Kiểm tra
Đăng xuất khỏi hệ thống.
Sau đó truy cập:
/posts/create
Kết quả:
302 Redirect
↓
/login
Đây là hành vi mặc định của Middleware auth.
Kết hợp với Breeze
Sau khi cài Laravel Breeze, Route dashboard thường được bảo vệ như sau:
Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth']);
Điều này đảm bảo chỉ người dùng đã xác thực mới truy cập được.
Middleware guest
Laravel cũng có Middleware:
guest
Ý nghĩa:
Ví dụ:
Route::middleware('guest')->group(function () {
Route::get(
'/login',
...
);
Route::get(
'/register',
...
);
});
Người đã đăng nhập( k phải guest) sẽ không thể mở lại trang Login hoặc Register.
Middleware nhiều lớp
Một Route có thể sử dụng nhiều Middleware.
Ví dụ:
Route::middleware([
'auth',
'verified',
])->group(function () {
//
});
Laravel sẽ kiểm tra lần lượt:
Đã đăng nhập.
Email đã xác minh.
Nếu một bước không đạt, yêu cầu sẽ bị chặn.
Tạo Middleware mới
Tạo bằng Artisan:
php artisan make:middleware CheckPostOwner
Laravel sẽ tạo:
app/
└── Http/
└── Middleware/
CheckPostOwner.php
Ví dụ Middleware
public function handle(
Request $request,
Closure $next
)
{
if (!auth()->check()) {
return redirect()
->route('login');
}
return $next($request);
}
$next($request) nghĩa là cho phép Request tiếp tục đi tới Controller.
Chỉ cho tác giả sửa bài
Giả sử mỗi Post có:
user_id
Middleware:
$post = $request->route('post');
if (
$post->user_id != auth()->id()
) {
abort(403);
}
return $next($request);
Nếu người dùng không phải là tác giả:
403 Forbidden
Đây là cách bảo vệ dữ liệu rất phổ biến trong Blog CMS.
Áp dụng cho Profile
Chú ý: ví dụ này chỉ minh họa vì Breeze thiết kế để mỗi user chỉ nhìn thấy profile của chính mình nên k cần middleware.
trước khi chạy link /profile nên dùng lệnh tạo CategoryController trước (do đã khai báo trong route nhưng chưa tạo dẫn đến lỗi trong route:list)
/profile/5/edit
Middleware kiểm tra:
$profile = $request
->route('profile');
if (
$profile->user_id != auth()->id()
) {
abort(403);
}
Người dùng chỉ được sửa Profile của chính mình.
Middleware trong thực tế
Một số Middleware thường gặp:
| Middleware | Chức năng |
|---|
| auth | Kiểm tra đăng nhập |
| guest | Chỉ cho khách truy cập |
| verified | Email đã xác minh |
| throttle | Giới hạn số lần gửi Request |
| signed | Kiểm tra URL có chữ ký |
| custom | Middleware tự tạo |
Middleware và Request
Quy trình xử lý:
Request
│
▼
Middleware
│
▼
Validation
│
▼
Controller
│
▼
Model
│
▼
Database
Middleware luôn hoạt động trước Controller.
Tổng kết
Trong bài này chúng ta đã học:
Middleware là tuyến phòng thủ đầu tiên của ứng dụng Laravel, giúp bảo vệ các chức năng quan trọng trước khi dữ liệu được xử lý.
Bài tập
Bảo vệ toàn bộ Route posts bằng Middleware auth.
Kiểm tra truy cập /dashboard khi chưa đăng nhập.
Tạo Middleware CheckPostOwner.
Chỉ cho phép tác giả sửa hoặc xóa bài viết của mình.
Áp dụng Middleware cho chức năng chỉnh sửa Profile.
Thử truy cập bằng tài khoản khác và quan sát kết quả 403 Forbidden.
Kết hợp auth và verified cho một Route bất kỳ.
Bài tiếp theo
Ở Bài 25, chúng ta sẽ tìm hiểu Authorization, cơ chế phân quyền trong Laravel bằng Gate và Policy, giúp xác định ai được phép thực hiện hành động nào trên từng đối tượng dữ liệu như Post, User hoặc Category.
BONUS
Phần view posts (hiện tại mới xây dựng có search theo category)
Có thể dùng CDN.
<link rel="stylesheet"
href="https://cdn.datatables.net/2.3.4/css/dataTables.dataTables.min.css">
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.datatables.net/2.3.4/js/dataTables.min.js"></script>
1. Table
<table id="postsTable" class="min-w-full">
<thead>
<tr>
<th>ID</th>
<th>Tiêu đề</th>
<th>User</th>
<th>Ngày đăng</th>
</tr>
</thead>
<tbody>
@foreach($posts as $post)
<tr>
<td>{{ $post->id }}</td>
<td>{{ $post->title }}</td>
<td>{{ $post->user->name }}</td>
<td>{{ $post->created_at->format('d/m/Y') }}</td>
</tr>
@endforeach
</tbody>
</table>
2. Khởi tạo
new DataTable('#postsTable', {
pageLength: 10,
responsive: true,
ordering: true,
searching: true,
info: true,
lengthChange: true
});
Là có ngay
3. Việt hóa
new DataTable('#postsTable', {
language: {
search: "🔍 Tìm kiếm:",
lengthMenu: "Hiển thị _MENU_ dòng",
info: "Hiển thị _START_ đến _END_ / _TOTAL_",
paginate: {
first: "Đầu",
last: "Cuối",
next: "Sau",
previous: "Trước"
},
zeroRecords: "Không tìm thấy dữ liệu",
emptyTable: "Chưa có dữ liệu"
}
});
4. Lúc này Controller chỉ cần
public function index()
{
$posts = Post::with('user')
->latest()
->get();
return view('posts.index', compact('posts'));
}
Không cần code search nữa vì DataTables search ngay trên trình duyệt.
5. Sau này nâng cấp
Khi số lượng bài viết lớn (ví dụ 10.000–100.000 bài), không nên tải tất cả về trình duyệt. Khi đó nên dùng Server-side Processing của DataTables:
return DataTables::eloquent(
Post::with('user')
)->toJson();
Kết hợp với gói Yajra DataTables trong Laravel để:
- Search ở database.
- Sort ở database.
- Pagination ở database.
- Tốc độ rất nhanh dù có hàng trăm nghìn bản ghi.
Mục 4 nên hiển thị all posts cho admin only...
Trong Laravel nên xử lý ở Controller bằng cách kiểm tra role/admin trước khi query.
Ví dụ bạn đang có cột role trong bảng users (admin, editor, user) như các bài trước, có thể làm:
use Illuminate\Support\Facades\Auth;
public function index()
{
$user = Auth::user();
if ($user->isAdmin()) {
// Admin xem tất cả bài viết
$posts = Post::with('user')
->latest()
->get();
} else {
// User thường chỉ xem bài của mình
$posts = Post::with('user')
->where('user_id', $user->id)
->latest()
->get();
}
return view('posts.index', compact('posts'));
}
Sau đó thêm function isAdmin() trong User Model:
app/Models/User.php
public function isAdmin()
{
return strtolower($this->role) === 'admin';
}
Khi đó logic sẽ là:
| User | Query |
|---|
| Admin | Post::all() → thấy tất cả posts |
| User | where('user_id', Auth::id()) → chỉ thấy posts của mình |
Có thể viết gọn hơn bằng toán tử 3 ngôi:
public function index()
{
$user = Auth::user();
$posts = Post::with('user')
->when(!$user->isAdmin(), function ($query) use ($user) {
$query->where('user_id', $user->id);
})
->latest()
->get();
return view('posts.index', compact('posts'));
}
Cách này Laravel style hơn vì chỉ có một query chain.
Sau này mở rộng thêm:
thì nên chuyển sang Laravel Policy + Gate thay vì kiểm tra trực tiếp trong Controller.
x1