Skip to Content

Middleware

Middleware (промежуточное ПО) — это фильтры, которые обрабатывают запросы до того, как они попадут в контроллер, и после того, как контроллер сформирует ответ.

Зачем нужен Middleware

Типичные задачи для middleware:

  • Проверка авторизации — пустить только авторизованных пользователей
  • Проверка прав — пустить только администраторов
  • Защита от CSRF — проверить токен формы
  • Ограничение запросов — защита от спама и DDoS
  • Логирование — записать информацию о запросе
  • Кеширование — вернуть закешированный ответ

Как это работает

Запрос пользователя ┌─────────────────┐ │ Middleware 1 │ ← Проверка авторизации └────────┬────────┘ ┌─────────────────┐ │ Middleware 2 │ ← Проверка прав доступа └────────┬────────┘ ┌─────────────────┐ │ Контроллер │ ← Обработка запроса └────────┬────────┘ ┌─────────────────┐ │ Middleware 2 │ ← Можно модифицировать ответ └────────┬────────┘ ┌─────────────────┐ │ Middleware 1 │ ← Можно модифицировать ответ └────────┬────────┘ Ответ пользователю

Каждый middleware может:

  1. Пропустить запрос дальше (вызвать $next($request))
  2. Прервать цепочку и вернуть свой ответ (например, “Доступ запрещён”)

Встроенные middleware

Flute CMS предоставляет готовые middleware для типичных задач:

Группы middleware

ГруппаЧто входитКогда использовать
defaultban.check, throttle, maintenanceПрименяется автоматически ко ВСЕМ маршрутам
webcsrf, throttleДля обычных страниц с формами
apithrottle, ban.checkДля API-эндпоинтов

Отдельные middleware

MiddlewareЧто делаетПример использования
authПускает только авторизованных#[Middleware(['auth'])]
guestПускает только гостейСтраница логина
csrfПроверяет CSRF-токенЗащита форм
can:правоПроверяет конкретное право#[Middleware(['can:manage_users'])]
throttleОграничивает кол-во запросовЗащита от спама
tokenПроверяет API-токенAPI-авторизация
htmxСпециальная обработка HTMXДля HTMX-запросов

Группа default применяется всегда. Даже если вы укажете withoutMiddleware(['throttle']), элементы из default не уберутся.

Настройка throttle

Middleware throttle защищает от слишком частых запросов:

// Формат: throttle:стратегия,лимит,интервал,политика // 60 запросов в минуту по IP #[Middleware(['throttle:ip,60,60'])] // 120 запросов в минуту по пользователю #[Middleware(['throttle:user,120,60'])] // Комбинация IP + пользователь #[Middleware(['throttle:ip_user,100,60'])]

Параметры:

  • Стратегия: ip, user, ip_user
  • Лимит: количество разрешённых запросов
  • Интервал: период в секундах (или строка: 1 minute)
  • Политика: fixed_window, sliding_window, token_bucket

Использование middleware

На всём контроллере

Атрибут #[Middleware] на классе применяется ко ВСЕМ методам:

<?php namespace Flute\Modules\Blog\Http\Controllers; use Flute\Core\Router\Annotations\Get; use Flute\Core\Router\Annotations\Middleware; use Flute\Core\Support\BaseController; /** * Все методы этого контроллера требуют авторизации. */ #[Middleware(['auth'])] class ProfileController extends BaseController { #[Get('/profile')] public function index() { // Сюда попадут только авторизованные пользователи return response()->view('profile::index'); } #[Get('/profile/settings')] public function settings() { // И сюда тоже return response()->view('profile::settings'); } }

На отдельном методе

Можно добавить middleware только к конкретному методу:

#[Middleware(['web'])] class ArticleController extends BaseController { /** * Просмотр статьи — доступен всем. */ #[Get('/articles/{id}')] public function show(int $id) { // middleware: web } /** * Редактирование статьи — только автору или админу. */ #[Get('/articles/{id}/edit')] #[Middleware(['auth', 'can:edit_articles'])] public function edit(int $id) { // middleware: web + auth + can:edit_articles } /** * Удаление — только с CSRF-проверкой. */ #[Post('/articles/{id}/delete')] #[Middleware(['auth', 'csrf'])] public function delete(int $id) { // middleware: web + auth + csrf } }

В файлах маршрутов

<?php // Отдельный маршрут с middleware router()->get('/admin', [AdminController::class, 'index']) ->middleware(['auth', 'can:admin_access']); // Группа маршрутов — middleware применяется ко всем router()->group(['middleware' => ['auth']], function () { router()->get('/dashboard', [DashboardController::class, 'index']); router()->get('/settings', [SettingsController::class, 'index']); }); // Вложенные группы router()->group(['prefix' => '/api', 'middleware' => ['api']], function () { // Публичные эндпоинты router()->get('/articles', [ArticleController::class, 'index']); // Защищённые эндпоинты router()->group(['middleware' => ['auth']], function () { router()->post('/articles', [ArticleController::class, 'store']); router()->delete('/articles/{id}', [ArticleController::class, 'destroy']); }); });

Создание своего middleware

Базовая структура

Создайте класс, реализующий MiddlewareInterface:

<?php namespace Flute\Modules\Blog\Middleware; use Closure; use Flute\Core\Router\Contracts\MiddlewareInterface; use Flute\Core\Support\FluteRequest; use Symfony\Component\HttpFoundation\Response; class CheckArticleOwnerMiddleware implements MiddlewareInterface { /** * Обработка запроса. * * @param FluteRequest $request Объект запроса * @param Closure $next Следующий middleware в цепочке * @param mixed ...$args Дополнительные параметры * @return Response */ public function handle(FluteRequest $request, Closure $next, ...$args): Response { // 1. Выполняем проверки ДО контроллера // Получаем ID статьи из URL $articleId = $request->getAttribute('id'); // Загружаем статью $article = rep(Article::class)->findByPK($articleId); // Статья не найдена? if (!$article) { return response()->json(['error' => 'Статья не найдена'], 404); } // Пользователь — не автор и не админ? if ($article->author_id !== user()->id && !user()->can('manage_articles')) { return response()->json(['error' => 'Нет прав на редактирование'], 403); } // 2. Можно передать данные в контроллер через атрибуты запроса $request->attributes->set('article', $article); // 3. Пропускаем запрос дальше $response = $next($request); // 4. Можно модифицировать ответ ПОСЛЕ контроллера // $response->headers->set('X-Custom-Header', 'value'); return $response; } }

Регистрация middleware

Зарегистрируйте middleware в провайдере модуля:

<?php namespace Flute\Modules\Blog\Providers; use Flute\Core\Support\ModuleServiceProvider; use Flute\Modules\Blog\Middleware\CheckArticleOwnerMiddleware; class BlogProvider extends ModuleServiceProvider { public function boot(\DI\Container $container): void { // Регистрируем middleware под коротким именем (алиасом) router()->aliasMiddleware('article.owner', CheckArticleOwnerMiddleware::class); $this->bootstrapModule(); } }

Использование своего middleware

#[Get('/articles/{id}/edit')] #[Middleware(['auth', 'article.owner'])] // Используем наш алиас public function edit(int $id) { // Статья уже загружена в middleware! $article = request()->getAttribute('article'); return response()->view('blog::edit', compact('article')); }

Примеры middleware

Логирование запросов

Записываем информацию о каждом запросе:

<?php namespace Flute\Modules\Analytics\Middleware; use Closure; use Flute\Core\Router\Contracts\MiddlewareInterface; use Flute\Core\Support\FluteRequest; use Symfony\Component\HttpFoundation\Response; class RequestLoggerMiddleware implements MiddlewareInterface { public function handle(FluteRequest $request, Closure $next, ...$args): Response { // Засекаем время начала $startTime = microtime(true); // Логируем входящий запрос logs('requests')->info('Запрос начат', [ 'method' => $request->getMethod(), 'url' => $request->getRequestUri(), 'ip' => $request->getClientIp(), 'user_id' => user()->isLoggedIn() ? user()->id : null, ]); // Пропускаем запрос дальше $response = $next($request); // Считаем время выполнения $duration = round((microtime(true) - $startTime) * 1000, 2); // Логируем результат logs('requests')->info('Запрос завершён', [ 'status' => $response->getStatusCode(), 'duration_ms' => $duration, ]); return $response; } }

Кеширование страниц

Возвращаем закешированный ответ для GET-запросов:

<?php namespace Flute\Modules\Cache\Middleware; use Closure; use Flute\Core\Router\Contracts\MiddlewareInterface; use Flute\Core\Support\FluteRequest; use Symfony\Component\HttpFoundation\Response; class PageCacheMiddleware implements MiddlewareInterface { public function handle(FluteRequest $request, Closure $next, int $ttl = 3600): Response { // Кешируем только GET-запросы if ($request->getMethod() !== 'GET') { return $next($request); } // Не кешируем для авторизованных (у них может быть персональный контент) if (user()->isLoggedIn()) { return $next($request); } // Формируем ключ кеша $cacheKey = 'page_' . md5($request->getRequestUri()); // Проверяем кеш $cached = cache()->get($cacheKey); if ($cached) { return $cached; // Возвращаем из кеша } // Выполняем запрос $response = $next($request); // Кешируем только успешные ответы if ($response->getStatusCode() === 200) { cache()->set($cacheKey, $response, $ttl); } return $response; } }

Использование:

#[Get('/articles')] #[Middleware(['page.cache:1800'])] // Кешировать на 30 минут public function index() { }

Проверка подписки

Пускаем только пользователей с активной подпиской:

<?php namespace Flute\Modules\Premium\Middleware; use Closure; use Flute\Core\Router\Contracts\MiddlewareInterface; use Flute\Core\Support\FluteRequest; use Symfony\Component\HttpFoundation\Response; class RequireSubscriptionMiddleware implements MiddlewareInterface { public function handle(FluteRequest $request, Closure $next, string $plan = null): Response { // Проверяем авторизацию if (!user()->isLoggedIn()) { if ($request->expectsJson()) { return response()->json(['error' => 'Требуется авторизация'], 401); } return redirect(route('login')); } // Проверяем подписку $subscription = user()->getSubscription(); if (!$subscription || !$subscription->isActive()) { if ($request->expectsJson()) { return response()->json(['error' => 'Требуется подписка'], 403); } return redirect(route('subscription.plans')) ->with('error', 'Для доступа к этому разделу нужна подписка'); } // Проверяем конкретный план (если указан) if ($plan && $subscription->plan !== $plan) { return response()->json([ 'error' => "Требуется план: {$plan}" ], 403); } return $next($request); } }

Использование:

#[Get('/premium/content')] #[Middleware(['subscription'])] // Любая подписка public function premiumContent() { } #[Get('/vip/content')] #[Middleware(['subscription:vip'])] // Только VIP-план public function vipContent() { }

CORS для API

Разрешаем запросы с других доменов:

<?php namespace Flute\Modules\Api\Middleware; use Closure; use Flute\Core\Router\Contracts\MiddlewareInterface; use Flute\Core\Support\FluteRequest; use Symfony\Component\HttpFoundation\Response; class CorsMiddleware implements MiddlewareInterface { // Разрешённые домены (* = все) private array $allowedOrigins = ['*']; // Разрешённые HTTP-методы private array $allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']; // Разрешённые заголовки private array $allowedHeaders = ['Content-Type', 'Authorization', 'X-Requested-With']; public function handle(FluteRequest $request, Closure $next, ...$args): Response { // Preflight-запрос (браузер проверяет, можно ли делать запрос) if ($request->getMethod() === 'OPTIONS') { $response = new Response('', 200); return $this->addCorsHeaders($response); } // Обычный запрос $response = $next($request); return $this->addCorsHeaders($response); } private function addCorsHeaders(Response $response): Response { $response->headers->set( 'Access-Control-Allow-Origin', implode(', ', $this->allowedOrigins) ); $response->headers->set( 'Access-Control-Allow-Methods', implode(', ', $this->allowedMethods) ); $response->headers->set( 'Access-Control-Allow-Headers', implode(', ', $this->allowedHeaders) ); $response->headers->set('Access-Control-Max-Age', '86400'); return $response; } }

Определение языка

Автоматически определяем язык пользователя:

<?php namespace Flute\Modules\I18n\Middleware; use Closure; use Flute\Core\Router\Contracts\MiddlewareInterface; use Flute\Core\Support\FluteRequest; use Symfony\Component\HttpFoundation\Response; class DetectLocaleMiddleware implements MiddlewareInterface { private array $supportedLocales = ['ru', 'en', 'de']; public function handle(FluteRequest $request, Closure $next, ...$args): Response { // Приоритет определения языка: // 1. Параметр в URL (?lang=en) // 2. Сохранённый в сессии // 3. Из заголовка браузера // 4. Язык по умолчанию $locale = $request->get('lang') ?? session()->get('locale') ?? $this->detectFromBrowser($request) ?? config('app.locale', 'ru'); // Проверяем, поддерживается ли язык if (!in_array($locale, $this->supportedLocales)) { $locale = 'ru'; } // Устанавливаем язык app()->setLocale($locale); session()->set('locale', $locale); return $next($request); } private function detectFromBrowser(FluteRequest $request): ?string { $header = $request->headers->get('Accept-Language'); if (!$header) { return null; } // Парсим заголовок: "ru-RU,ru;q=0.9,en;q=0.8" foreach (explode(',', $header) as $part) { $locale = explode(';', trim($part))[0]; $shortLocale = explode('-', $locale)[0]; if (in_array($shortLocale, $this->supportedLocales)) { return $shortLocale; } } return null; } }

Создание группы middleware

Если несколько middleware часто используются вместе, создайте группу:

public function boot(\DI\Container $container): void { // Регистрируем отдельные middleware router()->aliasMiddleware('subscription', RequireSubscriptionMiddleware::class); router()->aliasMiddleware('request.log', RequestLoggerMiddleware::class); // Создаём группу router()->middlewareGroup('premium.api', [ 'api', 'auth', 'subscription', 'request.log', 'throttle:user,100,60' ]); $this->bootstrapModule(); }

Использование:

#[Middleware(['premium.api'])] class PremiumApiController extends BaseController { // Все middleware из группы применяются автоматически }

Советы

Производительность

  • Кешируйте тяжёлые проверки — если проверяете права, кешируйте результат
  • Не делайте запросы к БД без необходимости — проверьте сначала простые условия
  • Используйте ленивую загрузку — не загружайте сервисы, пока они не нужны

Безопасность

  • Всегда валидируйте входные данные — даже в middleware
  • Логируйте подозрительную активность — слишком много ошибок авторизации, странные запросы
  • Не доверяйте данным из запроса — заголовки и параметры могут быть подделаны

Организация кода

  • Один middleware — одна задача — не смешивайте проверку авторизации с логированием
  • Давайте понятные именаcheck.subscription лучше, чем mw1
  • Документируйте параметры — если middleware принимает параметры, опишите их