Laravel 11 Middleware: Hướng Dẫn Cấu Hình Thực Chiến Từ A-Z

VMas-Dev-AnHuynh

Laravel Middleware Thực Chiến: Lá Chắn Bảo Vệ, Phân Quyền Và Kiểm Soát Request Đầu Vào

Khi xây dựng một ứng dụng web, việc kiểm soát ai có thể làm gì và đảm bảo dữ liệu đầu vào luôn an toàn là vô cùng quan trọng. Trong thế giới Laravel, Middleware chính là “người gác cổng” đắc lực, đứng giữa Client và hệ thống xử lý trung tâm (Controller) để lọc, kiểm tra và định tuyến các yêu cầu HTTP.

Nếu bạn là một lập trình viên PHP/Laravel, đặc biệt là ở trình độ Junior, chắc hẳn bạn đã từng tự hỏi: Làm sao để chặn người dùng chưa đăng nhập? Làm thế nào để chỉ có Admin mới được vào khu vực quản trị? Hay làm sao để tự động chặn địa chỉ IP xấu ngay từ cửa ngõ? Câu trả lời chính là Middleware.

Bài viết này sẽ là một cẩm nang thực chiến, giúp bạn không chỉ hiểu rõ mô hình “củ hành” (Onion Architecture) của Middleware mà còn biết cách tạo, cấu hình và đăng ký chúng một cách chính xác nhất trên Laravel 11. Chúng ta sẽ đi từ lý thuyết đến hai use-case thực tế:

  1. Phân quyền Admin – Chỉ cho phép tài khoản quản trị truy cập.
  2. Chặn IP đen (Blacklist) – Tự động từ chối những địa chỉ IP có hành vi phá hoại.

Mô hình củ hành HTTP: Middleware đứng ở đâu trong dự án Laravel?

Sơ đồ minh họa HTTP Pipeline trong Laravel: Request đi qua các lớp Middleware trước và sau khi đến Controller

Hãy tưởng tượng ứng dụng Laravel của bạn như một củ hành tây. Mỗi khi có một request HTTP được gửi đến, nó phải đi qua từng lớp vỏ trước khi chạm được vào nhân trung tâm – nơi chứa các logic nghiệp vụ trong Controller. Và chính các Middleware là những “lớp vỏ” này.

Quá trình này được gọi là HTTP Pipeline (đường ống xử lý HTTP). Request đi vào, lần lượt được xử lý bởi danh sách các Middleware đã đăng ký, mỗi Middleware có thể kiểm tra, sửa đổi, hoặc thậm chí từ chối request trước khi nó đến được Controller.

Sau khi Controller xử lý xong và tạo ra Response, Response này cũng sẽ đi qua các lớp Middleware một lần nữa (theo thứ tự ngược lại) trước khi được gửi về cho Client. Đây chính là lý do tại sao chúng ta có Middleware hoạt động ở cả giai đoạn trướcsau khi request xử lý.

Phân loại: Trước-Request Middleware vs Sau-Request Middleware

Middleware trong Laravel có thể được chia làm hai loại chính dựa trên thời điểm chúng thực thi logic của mình.

Loại Middleware Thời điểm thực thi Ứng dụng phổ biến
Trước-Request (Pre) Trước khi vào Controller Kiểm tra đăng nhập, phân quyền, xác thực token, chặn IP, ghi log request
Sau-Request (Post) Sau khi Controller xử lý, trước khi gửi response Thêm HTTP header bảo mật, nén dữ liệu, ghi log response

Ví dụ dưới đây minh họa cả hai loại trong cùng một middleware:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class BeforeAndAfterMiddleware
{
    public function handle(Request $request, Closure $next)
    {
        // 1. Code ở đây là TRƯỚC-REQUEST: Chạy trước khi vào Controller.
        if (!$request->hasHeader('X-Special')) {
            return response('Thiếu header đặc biệt', 400);
        }

        // Lời gọi $next($request) chuyển request sang middleware tiếp theo hoặc Controller.
        $response = $next($request);

        // 2. Code ở đây là SAU-REQUEST: Chạy sau khi Controller trả về response.
        $response->header('X-Processed-By', 'LaravelMiddleware');

        return $response;
    }
}

Từng bước xây dựng CheckAdminRole Middleware (Phân quyền cơ bản)

Bây giờ, chúng ta sẽ bắt tay vào xây dựng một Middleware có nhiệm vụ kiểm tra xem người dùng hiện tại có phải là Admin hay không.

Bước 1: Tạo Middleware bằng Artisan

php artisan make:middleware CheckAdminRole

Bước 2: Viết logic xử lý trong hàm handle

Mở file app/Http/Middleware/CheckAdminRole.php:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class CheckAdminRole
{
    public function handle(Request $request, Closure $next): Response
    {
        // Giả sử user đã đăng nhập và có cột 'role'
        if (!$request->user() || $request->user()->role !== 'admin') {
            if ($request->expectsJson()) {
                return response()->json(['message' => 'Bạn không có quyền truy cập.'], 403);
            }
            return redirect()->route('home')->with('error', 'Bạn không có quyền truy cập trang này.');
        }

        return $next($request);
    }
}

Giải thích code:

  • $request->user(): Lấy thông tin user đã xác thực (cần middleware auth chạy trước).
  • $request->expectsJson(): Xác định request API hay web để trả về JSON hoặc redirect.
  • return $next($request);: Rất quan trọng – cho phép request đi tiếp.

Bước 3: Sử dụng trên Route (cách nhanh nhất)

use App\Http\Middleware\CheckAdminRole;
use Illuminate\Support\Facades\Route;

Route::get('/admin/dashboard', function () {
    return 'Admin Dashboard';
})->middleware(CheckAdminRole::class);

Để gọn hơn, chúng ta sẽ đăng ký alias ở phần sau.

🔥 Use-case đặc biệt: Xây dựng CheckIpBlacklist Middleware (Chặn IP đen)

Đây là tình huống thực tế mà nhiều doanh nghiệp Việt Nam cần: Tự động chặn các địa chỉ IP có hành vi tấn công, spam, hoặc phá hoại.

Bước 1: Tạo Middleware

php artisan make:middleware CheckIpBlacklist

Bước 2: Viết logic kiểm tra IP

Mở file app/Http/Middleware/CheckIpBlacklist.php:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class CheckIpBlacklist
{
    /**
     * Danh sách IP bị chặn (có thể lấy từ database, file config, hoặc cache)
     */
    protected $blacklistedIps = [
        '192.168.1.100',
        '203.0.113.45',
        '198.51.100.99',
    ];

    public function handle(Request $request, Closure $next): Response
    {
        $clientIp = $request->ip();

        // Kiểm tra IP có nằm trong danh sách đen không
        if (in_array($clientIp, $this->blacklistedIps)) {
            // Log lại hành vi để phân tích sau
            \Log::warning('IP bị chặn truy cập', ['ip' => $clientIp, 'url' => $request->fullUrl()]);

            // Trả về lỗi 403 Forbidden
            return response()->json([
                'message' => 'Truy cập bị từ chối. IP của bạn nằm trong danh sách đen.',
                'code' => 'IP_BLOCKED'
            ], 403);
        }

        return $next($request);
    }
}

Giải thích code:

  • $request->ip(): Lấy địa chỉ IP thực của client, an toàn hơn so với đọc từ header X-Forwarded-For.
  • in_array(): So sánh IP với danh sách đen (trong thực tế nên lưu trong database hoặc Redis để dễ quản lý).
  • \Log::warning(): Ghi log để giám sát các hành vi đáng ngờ.
  • Trả về JSON với mã 403 – phù hợp cho cả API và ứng dụng SPA.

Bước 3: Nâng cấp – Lấy danh sách IP đen từ Database

Để linh hoạt, bạn có thể tạo bảng ip_blacklists và model tương ứng, sau đó sử dụng trong middleware:

use App\Models\IpBlacklist;

public function handle(Request $request, Closure $next)
{
    $clientIp = $request->ip();

    // Kiểm tra trong database
    if (IpBlacklist::where('ip', $clientIp)->exists()) {
        abort(403, 'IP của bạn đã bị chặn.');
    }

    return $next($request);
}

Response lỗi 403 khi middleware chặn IP trong danh sách đen

Cuộc cách mạng Laravel 11: Đăng ký Middleware tại file bootstrap/app.php

So sánh cách đăng ký Middleware trong Laravel 10 (qua Kernel.php) và Laravel 11 (qua bootstrap/app.php)

Trong Laravel 11, toàn bộ cấu hình middleware được quản lý tập trung tại file bootstrap/app.php với method withMiddleware.

Đăng ký Alias (bí danh) cho Middleware

// bootstrap/app.php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use App\Http\Middleware\CheckAdminRole;
use App\Http\Middleware\CheckIpBlacklist;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php', // nếu có
    )
    ->withMiddleware(function (Middleware $middleware) {
        // Đăng ký alias (bí danh) cho middleware
        $middleware->alias([
            'admin' => CheckAdminRole::class,
            'ip.blacklist' => CheckIpBlacklist::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        // ...
    })->create();

Sử dụng Alias trên Route

// routes/web.php hoặc routes/api.php
use Illuminate\Support\Facades\Route;

Route::middleware(['auth', 'admin'])->group(function () {
    Route::get('/admin/dashboard', [AdminController::class, 'dashboard']);
    Route::get('/admin/users', [AdminController::class, 'users']);
});

// Chặn IP đen cho toàn bộ ứng dụng (global) nhưng cần đăng ký global middleware
// Hoặc áp dụng riêng cho một nhóm route nhạy cảm:
Route::middleware(['ip.blacklist'])->group(function () {
    Route::post('/checkout', [OrderController::class, 'checkout']);
    Route::post('/login', [AuthController::class, 'login']);
});

Đăng ký Global Middleware (áp dụng cho mọi request)

Nếu muốn middleware chạy trên tất cả các route (ví dụ: ip.blacklist), bạn dùng method append hoặc prepend:

->withMiddleware(function (Middleware $middleware) {
    // Thêm vào cuối danh sách global middleware
    $middleware->append(CheckIpBlacklist::class);

    // Hoặc thêm vào đầu danh sách
    $middleware->prepend(AnotherGlobalMiddleware::class);
})

⚠️ Lỗi thường gặp: Quên return $next($request); – “Sát thủ thầm lặng”

Hình ảnh màn hình trắng trong Laravel do lỗi quên return $next($request) trong middleware

Nguyên nhân: Bạn quên không chuyển tiếp request cho middleware hoặc controller tiếp theo.
Hậu quả: Trình duyệt hiển thị màn hình trắng xóa (White Screen of Death).
Cách khắc phục: Luôn đảm bảo mọi nhánh logic đều trả về Response.

// Đúng
public function handle(Request $request, Closure $next)
{
    if ($condition) {
        return redirect('/');   // có return
    }
    return $next($request);     // có return
}

// Sai - thiếu return
public function handle(Request $request, Closure $next)
{
    if ($condition) {
        return redirect('/');
    }
    $next($request); // ❌ thiếu return -> white screen
}

✅ Best Practices cho Middleware

  1. Single Responsibility: Mỗi middleware chỉ nên làm một việc. Không viết logic nghiệp vụ phức tạp (thanh toán, xử lý dữ liệu lớn) trong middleware.
  2. Tận dụng Middleware Parameters để tái sử dụng: Ví dụ một middleware CheckRole có thể nhận tham số role động.
    // Middleware
    public function handle(Request $request, Closure $next, string $role)
    {
    if (!$request->user() || $request->user()->role !== $role) {
        abort(403);
    }
    return $next($request);
    }
    // Sử dụng: middleware('role:admin'), middleware('role:editor')
  3. Ưu tiên dùng alias: Code ngắn gọn, dễ bảo trì hơn so với tên class dài.
  4. Log hành vi bất thường: Khi chặn IP hoặc từ chối truy cập, hãy ghi log để phân tích sau.

FAQ (Các câu hỏi thường gặp)

1. Làm sao để loại bỏ (exclude) một vài route ra khỏi danh sách bộ lọc của Global Middleware?

Sử dụng method withoutMiddleware:

Route::get('/webhook/payment', [WebhookController::class, 'handle'])
    ->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class, 'ip.blacklist']);

2. Terminable Middleware là gì và nó được ứng dụng khi nào?

TerminableMiddleware là middleware có method terminate(), chạy sau khi response đã được gửi về client. Ứng dụng cho các tác vụ nặng nhưng không cần client chờ đợi: ghi log chi tiết, gửi email, khởi tạo queue job.

class MyTerminableMiddleware
{
    public function handle($request, $next) { return $next($request); }

    public function terminate($request, $response)
    {
        \Log::info('Request đã hoàn thành', ['url' => $request->fullUrl()]);
    }
}

Tổng kết và lộ trình chinh phục tiếp theo

Middleware là lớp bảo vệ đầu tiên và mạnh mẽ nhất trong ứng dụng Laravel. Với hai ví dụ thực chiến phân quyền Adminchặn IP đen, bạn đã có thể kiểm soát luồng request một cách linh hoạt, bảo vệ hệ thống khỏi các truy cập trái phép.

📌 Ghi nhớ:

  • Laravel 11 không còn Kernel.php – hãy cấu hình trong bootstrap/app.php.
  • Luôn return $next($request) để tránh lỗi white screen.
  • Giữ middleware gọn nhẹ, tận dụng tham số để tái sử dụng.

📚 Tham khảo chính thức:

Chia sẻ bài viết này
By VMas-Dev-AnHuynh Software Engineer
Follow:
SOFTWARE ENGINEER / FULL-STACK DEVELOPER