🧱 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 — ваш.
Полное имя класса передавать нельзя
// ✅ работает
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
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
namespace PageBlocks\App\Http\Middleware;
use Boshnik\PageBlocks\Http\Request;
class ExampleMiddleware extends Middleware
{
public function handle(Request $request)
{
return true;
}
}Что вернуть из handle():
| Возврат | Что произойдёт |
|---|---|
true | Маршрут выполняется дальше |
false | 403 |
abort($code) | Завершение с этим кодом |
response(...) | Отправляется ваш ответ — например redirect('/') |
Middleware с параметром
Параметр пишется через двоеточие и попадает в setParams():
Route::get('api/v1/orders', 'OrderApi@index')->middleware('ApiAuth:orders.read');Двоеточие разделяет имя и параметры по первому вхождению. Полные имена классов содержат обратные слэши, но не двоеточия, поэтому разбор однозначен.
🔗 Как навешивать
На один маршрут
Route::get('profile', 'UserController@profile')->middleware('auth');На группу
Route::prefix('account')
->middleware(['auth', 'csrf'])
->group(function () {
Route::get('/', 'AccountController@index');
Route::post('update', 'AccountController@update');
});На весь набор CRUD
Route::crud('mgr/pb/constructor/blocks', BlockController::class)
->middleware(['modauth', 'SudoOnly']);Вешается на все одиннадцать маршрутов пачки — см. Route::crud().
🚫 Исключение
Route::post('webhook', 'WebhookController@handle')->withoutMiddleware('csrf');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-токена сайта.