Skip to content

🧱 Middleware

Промежуточный слой, который выполняется до контроллера: проверка доступа, авторизация, защита от CSRF, ограничение частоты.

📚 Готовые middleware

ПсевдонимЧто делает
authПускает только авторизованного пользователя сайта, иначе 401
guestТолько для анонимных; авторизованного уводит на /
modauthПроверяет токен менеджера в заголовке modauth, иначе 403
csrfСверяет CSRF-токен на POST, PUT, PATCH, DELETE
g-recaptcha-v3Проверяет reCAPTCHA v3 на изменяющих запросах
apiТребует bearer-токен и путь, начинающийся с /api/
throttleОграничение частоты

Плюс два из фреймворка, у них псевдонима нет — пишутся по имени класса:

ИмяЧто делает
ApiAuthТокен REST API; принимает право: ApiAuth:cargo.read
SudoOnlyТолько пользователь менеджера с sudo или из группы администраторов

⚙️ Как имя превращается в класс

Псевдоним разворачивается в имя класса, а пространство имён приписывается при исполнении: сперва пробуется PageBlocks\App\Http\Middleware\, затем Boshnik\PageBlocks\Http\Middleware\. Поэтому один и тот же короткий SudoOnly находит класс фреймворка, а Authenticate — ваш.

Полное имя класса передавать нельзя

php
// ✅ работает
Route::get('profile', 'UserController@profile')->middleware('auth');
Route::get('profile', 'UserController@profile')->middleware('Authenticate');

// ❌ 403: к полному имени всё равно припишется префикс
Route::get('profile', 'UserController@profile')->middleware(Authenticate::class);

Передали FQCN — получите PageBlocks\App\Http\Middleware\PageBlocks\App\Http\Middleware\Authenticate, такого класса нет, и запрос отклоняется.

Ненайденный middleware отклоняет запрос

Опечатка в имени — это запись уровня ERROR в логе MODX и 403, а не пропуск. Fail-closed здесь намеренно: охрана, которой нет, не должна выглядеть как охрана, которая пропустила.

Это ровно противоположно поведению throttle:имя — несуществующий лимитер запрос пропускает. Разница в цене ошибки: пропущенная проверка доступа открывает данные, а пропущенное ограничение частоты — нет.

🧩 Псевдонимы

Живут в core/App/bootstrap/app.php — это слой сайта, обновление компонента его не трогает:

php
<?php

return [
    'middleware' => [
        'modauth' => MODAuthenticate::class,
        'csrf' => VerifyCsrfToken::class,
        'g-recaptcha-v3' => VerifyGoogleRecaptchaV3::class,
        'auth' => Authenticate::class,
        'guest' => Guest::class,
        'api' => API::class,
    ]
];

В этом файле не должно быть use

Объявления namespace здесь нет намеренно, поэтому Authenticate::class даёт строку 'Authenticate' — то самое имя, к которому исполнение припишет префикс.

Допишете use PageBlocks\App\Http\Middleware\Authenticate; — и ::class начнёт отдавать полное имя. После приписывания префикса класс не найдётся, и каждый маршрут с этим псевдонимом станет отдавать 403.

🛠 Свой middleware

Класс в core/App/Http/Middleware/, унаследованный от Middleware:

php
<?php

namespace PageBlocks\App\Http\Middleware;

use Boshnik\PageBlocks\Http\Request;

class ExampleMiddleware extends Middleware
{
    public function handle(Request $request)
    {
        return true;
    }
}

Что вернуть из handle():

ВозвратЧто произойдёт
trueМаршрут выполняется дальше
false403
abort($code)Завершение с этим кодом
response(...)Отправляется ваш ответ — например redirect('/')

Middleware с параметром

Параметр пишется через двоеточие и попадает в setParams():

php
Route::get('api/v1/orders', 'OrderApi@index')->middleware('ApiAuth:orders.read');

Двоеточие разделяет имя и параметры по первому вхождению. Полные имена классов содержат обратные слэши, но не двоеточия, поэтому разбор однозначен.

🔗 Как навешивать

На один маршрут

php
Route::get('profile', 'UserController@profile')->middleware('auth');

На группу

php
Route::prefix('account')
    ->middleware(['auth', 'csrf'])
    ->group(function () {
        Route::get('/', 'AccountController@index');
        Route::post('update', 'AccountController@update');
    });

На весь набор CRUD

php
Route::crud('mgr/pb/constructor/blocks', BlockController::class)
    ->middleware(['modauth', 'SudoOnly']);

Вешается на все одиннадцать маршрутов пачки — см. Route::crud().

🚫 Исключение

php
Route::post('webhook', 'WebhookController@handle')->withoutMiddleware('csrf');
php
Route::middleware(['auth', 'csrf'])
    ->withoutMiddleware('csrf')
    ->group(function () {
        Route::post('settings', 'SettingsController@update');
    });

📌 Что навешивается само

На каждый POST, PUT, PATCH и DELETE вне контекста mgr автоматически добавляются два middleware:

  • csrf — если он не исключён через withoutMiddleware;
  • g-recaptcha-v3 — всегда.

Указывать их вручную не нужно. reCAPTCHA при пустой настройке pageblocks_recaptcha_secret_key пропускает запрос, поэтому на сайте без ключей она ничего не меняет.

Включается заполнением ключей, а не галочкой

Установщик заводит pageblocks_recaptcha_service, pageblocks_recaptcha_public_key и pageblocks_recaptcha_secret_key пустыми. Пустой секретный ключ и означает «выключено», поэтому сразу после установки reCAPTCHA ничему не мешает. Вписали ключи — заработала.

Контекст mgr исключён потому, что менеджер защищён своим токеном (modauth), а его запросы не несут csrf-токена сайта.

© PageBlocks 2019-present