Toidicode.com

Toidicode.com

BASIC TO ADVANCE

Bài 9: Middleware trong Laravel 13

Ở bài trước TaskFlow đã có form tạo project. 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ề middleware trong Laravel 13: cách tạo, đăng ký, truyền tham số, gắn vào controller bằng attribute và cơ chế chống CSRF.

1. Middleware là gì?

Middleware là các lớp xử lý nằm giữa request và ứng dụng. Các bạn có thể hình dung chúng như các lớp vỏ hành: request đi qua lần lượt từng lớp đến route/controller, rồi response đi ngược ra qua đúng các lớp đó, như vòng đời request ở Bài 2.

Mỗi lớp có thể kiểm tra request và cho đi tiếp, chặn lại bằng cách tự trả về response (VD chưa đăng nhập thì chuyển sang trang login), hoặc sửa response sau khi controller chạy xong. Chúng ta đã dùng middleware throttle có sẵn ở Bài 4, bài này sẽ tự viết middleware của mình.

2. Tạo middleware.

Để tạo middleware các bạn dùng lệnh artisan sau, file được tạo trong thư mục app/Http/Middleware:

php artisan make:middleware LogRequests

VD: Mình viết middleware LogRequests ghi lại method, path, status code và thời gian xử lý của mỗi request vào log.

- app/Http/Middleware/LogRequests.php

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;

class LogRequests
{
    public function handle(Request $request, Closure $next): Response
    {
        // trước khi request vào controller
        $start = microtime(true);

        $response = $next($request);

        // sau khi controller đã trả về response
        $ms = (int) ((microtime(true) - $start) * 1000);

        Log::info(sprintf(
            '%s /%s %d %dms',
            $request->method(),
            $request->path(),
            $response->getStatusCode(),
            $ms,
        ));

        return $response;
    }
}

Trong đó:

  • $next($request) chuyển request vào lớp tiếp theo và trả về response. Code đặt trước dòng này chạy trước controller, code đặt sau chạy khi đã có response.
  • Muốn chặn request thì không gọi $next mà trả về luôn một response, như middleware ở phần 4.
  • Log::info() ghi một dòng vào storage/logs/laravel.log. Logging sẽ được nói kỹ ở Bài 11.

Middleware cũng inject được class qua constructor như controller ở Bài 5.

Terminable middleware.

Nếu middleware có method terminate(), Laravel sẽ gọi nó sau khi response đã gửi về trình duyệt (với web server dùng FastCGI như PHP-FPM), hợp với các việc không cần người dùng phải chờ:

public function terminate(Request $request, Response $response): void
{
    // chạy sau khi response đã được gửi đi
}

3. Đăng ký middleware.

Cách đơn giản nhất là truyền tên class vào middleware() của route hoặc group như ở Bài 4, VD ->middleware(LogRequests::class). Ngoài ra mọi cấu hình middleware đều nằm trong withMiddleware() của file bootstrap/app.php, mỗi dòng dưới đây là một cách đăng ký khác nhau, các bạn chỉ chọn một chứ không dán cả khối:

->withMiddleware(function (Middleware $middleware): void {
    // global: chạy với mọi request
    $middleware->append(LogRequests::class);   // thêm vào cuối
    $middleware->prepend(LogRequests::class);  // thêm vào đầu

    // thêm vào nhóm web hoặc api
    $middleware->web(append: [LogRequests::class]);
    $middleware->api(prepend: [LogRequests::class]);

    // đặt tên ngắn (alias)
    $middleware->alias([
        'log.requests' => LogRequests::class,
    ]);
})
  • Global middleware chạy với mọi request, kể cả khi không có route nào khớp. VD TrimStrings, ConvertEmptyStringsToNull có sẵn ở bài trước.
  • Middleware group gom nhiều middleware dưới một tên. Nhóm web (cookie, session, chống CSRF...) được tự động gắn cho routes/web.php, nhóm api cho routes/api.php.
  • Alias là tên ngắn thay cho tên class, như throttle, auth là các alias có sẵn.

Với TaskFlow, mình chỉ muốn ghi log các trang project nên chỉ dùng alias. Khối withMiddleware() cuối cùng của bài (gồm cả alias limit.projects của phần 4) như sau:

- bootstrap/app.php

use App\Http\Middleware\LimitProjects;
use App\Http\Middleware\LogRequests;
// ... các use có sẵn

    ->withMiddleware(function (Middleware $middleware): void {
        $middleware->alias([
            'log.requests' => LogRequests::class,
            'limit.projects' => LimitProjects::class,
        ]);
    })

Rồi thêm alias vào group project:

- routes/web.php

Route::middleware(['throttle:projects', 'log.requests'])
    ->whereNumber('id')
    ->group(function () {
        // các route project như cũ
    });

Mở vài trang project rồi xem file log:

tail storage/logs/laravel.log
# [2026-11-14 14:33:11] local.INFO: GET /projects 200 54ms
# [2026-11-14 14:33:11] local.INFO: GET /projects/1 200 5ms
# [2026-11-14 14:33:12] local.INFO: GET /projects/9/tasks 200 0ms

Trang chủ / không có trong group nên không được ghi. Nếu cần bỏ một middleware cho riêng route nào đó trong group, các bạn dùng ->withoutMiddleware('log.requests').

4. Tham số và attribute #[Middleware].

Middleware có thể nhận thêm tham số, khai báo sau $next trong handle(). Khi gắn vào route, tham số được viết sau dấu :, nhiều tham số cách nhau bởi dấu ,, giống throttle:3,1 ở Bài 4.

VD: Vì project mới đang lưu trong session, mình giới hạn mỗi người chỉ được tạo tối đa $max project.

- app/Http/Middleware/LimitProjects.php

public function handle(Request $request, Closure $next, string $max = '5'): Response
{
    if (count(session('projects', [])) >= (int) $max) {
        // dừng ở đây, request không đến được controller
        return to_route('projects.index')
            ->with('status', "Các bạn chỉ được tạo tối đa {$max} project.");
    }

    return $next($request);
}

Tham số luôn là chuỗi nên mình ép sang số bằng (int).

Middleware này chỉ cần cho hai action create, store. Thay vì tách route, Laravel 13 cho phép gắn middleware ngay trên controller bằng attribute #[Middleware]:

- app/Http/Controllers/ProjectController.php

use Illuminate\Routing\Attributes\Controllers\Middleware;

#[Middleware('limit.projects:3', only: ['create', 'store'])]
class ProjectController extends Controller
{
    // ...
}

Trong đó:

  • Tham số đầu là middleware (alias hoặc tên class), only giới hạn các method được áp dụng, except thì ngược lại.
  • Attribute đặt được nhiều lần trên class và trên từng method, tất cả được gộp với middleware khai báo ở route.
php artisan route:list -v --path=projects/create
#   GET|HEAD       projects/create .. projects.create › ProjectController@create
#                  ⇂ web
#                  ⇂ throttle:projects
#                  ⇂ log.requests
#                  ⇂ limit.projects:3

Lúc này khi session đã có 3 project, mở /projects/create sẽ bị chuyển về /projects kèm thông báo giới hạn.

5. Chống CSRF với PreventRequestForgery.

CSRF là kiểu tấn công lợi dụng việc người dùng đang đăng nhập, để một trang web khác tự gửi form đến ứng dụng của chúng ta. Laravel chống lại bằng middleware PreventRequestForgery nằm trong nhóm web. Middleware này bỏ qua các request GET, HEAD, OPTIONS, còn với POST, PUT, PATCH, DELETE nó kiểm tra hai lớp như mình đã giới thiệu ở Bài 1:

  • Header Sec-Fetch-Site do trình duyệt tự gửi (chỉ qua HTTPS): bằng same-origin thì request được cho qua luôn.
  • Nếu không, request phải có CSRF token khớp với token trong session, gửi bằng input _token (directive @csrf ở Bài 8) hoặc header X-CSRF-TOKEN khi gọi bằng JavaScript. Sai hoặc thiếu token sẽ nhận lỗi 419.
curl -s -X POST http://localhost:8000/projects -d "name=Test" -o /dev/null -w "%{http_code}\n"
# 419

curl -s -X POST http://localhost:8000/projects -d "name=Test" -o /dev/null -w "%{http_code}\n" \
    -H "Sec-Fetch-Site: same-origin"
# 302

Lệnh thứ hai qua được vì curl tự đặt header, nhưng lớp bảo vệ không vì thế mà yếu đi: header Sec-* do chính trình duyệt đặt, JavaScript của trang khác không sửa được, còn curl không phải trình duyệt của nạn nhân nên không có cookie đăng nhập của họ để lợi dụng.

Request bị 419 không xuất hiện trong log của LogRequests, vì nhóm web chạy trước middleware của group project: lớp ngoài đã chặn thì request không vào được các lớp bên trong.

Với những URI nhận request từ dịch vụ bên ngoài (VD webhook thanh toán) không thể gửi kèm token, các bạn loại trừ chúng trong bootstrap/app.php. Các route này mất lớp chống CSRF nên phải tự kiểm tra chữ ký/secret của dịch vụ gửi webhook:

->withMiddleware(function (Middleware $middleware): void {
    $middleware->preventRequestForgery(except: [
        'webhooks/*',
    ]);
})

Method preventRequestForgery() còn nhận originOnly: true (chỉ kiểm tra origin, sai thì 403) và allowSameSite: true (chấp nhận cả request từ subdomain, Sec-Fetch-Site: same-site).

6. Lời kết.

Như vậy chúng ta đã tìm hiểu xong middleware trong Laravel 13. Các bạn có thể xem thêm tại tài liệu chính thức. Form tạo project hiện vẫn kiểm tra dữ liệu bằng tay, bài sau mình sẽ giới thiệu về validation để làm việc này gọn và đầy đủ hơn.

Đă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