IO v7. Middleware и работа с ними
Описание
Middleware (промежуточное ПО) предоставляет удобный механизм для фильтрации HTTP-запросов, поступающих в приложение. Middleware выполняются до и после вызова экшена роута, что позволяет:
- Проверять авторизацию пользователя
- Добавлять CORS заголовки
- Логировать запросы
- Модифицировать входящие запросы или исходящие ответы
- Ограничивать доступ по IP
- И многое другое
Фреймворк также предоставляет встроенную поддержку CORS (Cross-Origin Resource Sharing) с автоматической обработкой preflight (OPTIONS) запросов.
Middleware используются в Route.
Интерфейс Middleware
Все middleware должны реализовывать интерфейс IO\Middleware:
<?php
namespace IO;
interface Middleware
{
/**
* Обработка запроса до выполнения роута
*
* @param App $app Экземпляр приложения
* @param array $params Параметры middleware
* @return mixed|null Если возвращает не null, выполнение прерывается
*/
public function handle($app, $params = []);
/**
* Обработка после выполнения роута (опционально)
*
* @param App $app Экземпляр приложения
* @param mixed $response Ответ роута
* @param array $params Параметры middleware
* @return void
*/
public function terminate($app, $response, $params = []);
}
Методы Route для работы с Middleware
addMiddleware()
Добавляет middleware в цепочку выполнения для текущего роута программно.
/**
* Добавить middleware в цепочку
*
* @param string|Middleware $middleware Класс middleware (например, \IO\Middleware\Auth::class) или объект
* @param array $params Параметры для middleware
* @return self Возвращает текущий объект роута для цепочечного вызова
*/
protected function addMiddleware($middleware, $params = [])
Параметры:
$middleware- полное имя класса middleware или объект, реализующий интерфейсMiddleware$params- ассоциативный массив параметров, которые будут переданы в методыhandle()иterminate()
Примеры:
<?php
namespace App\Routes;
use IO\Route;
class DashboardRoute extends Route
{
public function init()
{
parent::init();
// Простая авторизация
$this->addMiddleware(\IO\Middleware\Auth::class);
// Авторизация с параметрами
$this->addMiddleware(\IO\Middleware\Auth::class, [
'redirect' => '/login',
'json' => false
]);
// Несколько middleware
$this->addMiddleware(\IO\Middleware\Cors::class, [
'allow_origin' => ['https://example.com']
]);
$this->addMiddleware(\App\Middleware\CheckPermission::class, [
'permission' => 'dashboard.view',
'log' => true
]);
}
public function actionIndex()
{
return $this->render('@app/dashboard.twig');
}
}
Встроенные middleware
IO\Middleware\AuthMiddleware
Проверяет авторизацию пользователя.
Пространство имен: IO\Middleware\AuthMiddleware
Параметры:
redirect- string - URL для перенаправления неавторизованных пользователей (по умолчанию '/login')json- bool - если true, возвращает JSON ответ вместо редиректа (по умолчанию false)
Примеры:
// Редирект на страницу логина
$app->add_route([
"url" => "/cabinet",
"name" => "app:cabinet",
"method" => "actionIndex",
"middleware" => [
\IO\Middleware\AuthMiddleware::class => [
'redirect' => '/login'
]
]
]);
// API с JSON ответом при ошибке
$app->add_route([
"url" => "/api/user/data",
"name" => "app:api:userdata",
"method" => "actionUserData",
"middleware" => [
\IO\Middleware\AuthMiddleware::class => [
'json' => true
]
]
]);
Поддержка CORS
Фреймворк предоставляет встроенную поддержку CORS (Cross-Origin Resource Sharing) без необходимости использования middleware.
Конфигурация CORS в роуте
Для включения CORS добавьте ключ cors в конфигурацию роута:
$app->add_route([
"url" => "/api/data",
"name" => "app:api:data",
"method" => "actionData",
"cors" => [
'allow_origin' => ['https://client.com', 'https://app.com'],
'allow_credentials' => true,
'allow_methods' => ['GET', 'POST'],
'allow_headers' => ['Content-Type', 'Authorization'],
'max_age' => 3600,
]
]);
Параметры CORS
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
allow_origin | array | [] | Список разрешенных источников (доменов) |
allow_methods | array | ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'] | Разрешенные HTTP методы |
allow_headers | array | ['Content-Type', 'Authorization', 'X-Requested-With', 'Accept'] | Разрешенные заголовки |
allow_credentials | bool | false | Разрешить отправку кук и HTTP-авторизацию |
max_age | int | 86400 | Время кеширования preflight запроса (в секундах) |
expose_headers | array | [] | Заголовки, доступные для чтения в браузере |
Автоматическая обработка OPTIONS
Фреймворк автоматически обрабатывает OPTIONS (preflight) запросы для роутов с конфигурацией CORS:
- При получении OPTIONS запроса, фреймворк находит соответствующий роут по URL
- Устанавливает CORS заголовки из конфигурации
- Возвращает ответ 200 без вызова экшена роута
- Для не-OPTIONS запросов CORS заголовки устанавливаются перед выполнением экшена
Утилита CorsTrait
Для удобной генерации конфигурации CORS на основе глобальной переменной $ioProjects предоставляется трейт IO\CorsTrait:
<?php
namespace App\Routes;
use IO\Route;
use IO\CorsTrait;
class ApiRoute extends Route
{
use CorsTrait;
public static function routes($app)
{
$instance = new self();
// Автоматическая генерация origins из $ioProjects
$corsConfig = $instance->getCorsConfig('mobi');
$app->add_route([
"url" => "/api/user/data",
"name" => "app:api:userdata",
"method" => "actionUserData",
"cors" => $corsConfig,
"middleware" => [
\IO\Middleware\AuthMiddleware::class => ['json' => true]
]
]);
}
}
Метод getCorsConfig()
/**
* Получить конфигурацию CORS для проекта
*
* @param string|null $project Имя проекта (например, 'mobi', 'admin')
* @param array $extraOrigins Дополнительные источники для добавления
* @return array Конфигурация CORS
*/
protected function getCorsConfig($project = null, $extraOrigins = [])
Автоматически включает:
- Все проекты из $ioProjects (с https и http для dev-среды)
- Специфичный домен для указанного проекта
- Дополнительные источники из параметра $extraOrigins
Создание собственного middleware
Пример: Middleware для логирования
<?php
namespace App\Middleware;
use IO\Middleware;
class LoggerMiddleware implements Middleware
{
protected $logFile = '/var/log/requests.log';
public function handle($app, $params = [])
{
if (isset($params['log_file']))
{
$this->logFile = $params['log_file'];
}
$this->log('START ' . $_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI']);
return null;
}
public function terminate($app, $response, $params = [])
{
$this->log('END ' . $_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI'] .
' - ' . http_response_code());
}
protected function log($message)
{
$date = date('Y-m-d H:i:s');
$ip = getRealIP();
file_put_contents($this->logFile, "[$date] [$ip] $message\n", FILE_APPEND);
}
}
Пример: Middleware для проверки IP
<?php
namespace App\Middleware;
use IO\Middleware;
class IpFilterMiddleware implements Middleware
{
public function handle($app, $params = [])
{
$ip = getRealIP();
$whitelist = $params['whitelist'] ?? [];
$blacklist = $params['blacklist'] ?? [];
$redirect = $params['redirect'] ?? '/403';
$json = $params['json'] ?? false;
// Проверка черного списка
if (in_array($ip, $blacklist))
{
return $this->deny('IP is blacklisted', $redirect, $json);
}
// Если есть белый список, проверяем вхождение
if (!empty($whitelist) && !in_array($ip, $whitelist))
{
return $this->deny('IP not in whitelist', $redirect, $json);
}
return null;
}
protected function deny($message, $redirect, $json)
{
if ($json)
{
return [
'error_code' => -12,
'error_str' => $message
];
}
header("Location: $redirect");
exit;
}
public function terminate($app, $response, $params = []) {}
}
Использование кастомных middleware
<?php
namespace App\Routes;
use IO\Route;
class AdminRoute extends Route
{
public static function routes($app)
{
$app->add_route([
"url" => "/admin",
"name" => "app:admin",
"method" => "actionIndex",
"middleware" => [
// Проверка IP
\App\Middleware\IpFilter::class => [
'whitelist' => ['192.168.1.0/24', '10.0.0.1'],
'redirect' => '/403'
],
// Авторизация
\IO\Middleware\AuthMiddleware::class => [
'redirect' => '/login'
],
// Логирование
\App\Middleware\Logger::class => [
'log_file' => '/var/log/admin.log'
]
]
]);
}
}
Порядок выполнения
Middleware выполняются в следующем порядке:
- Middleware из конфигурации роута (в порядке объявления)
- Middleware, добавленные через addMiddleware() (в порядке добавления)
Для каждого middleware:
- Сначала вызывается метод handle() (этап before)
- Выполняется экшен роута
- Затем для каждого middleware (в обратном порядке) вызывается метод terminate() (этап after)
$this->addMiddleware(MiddlewareA::class);
$this->addMiddleware(MiddlewareB::class);
$this->addMiddleware(MiddlewareC::class);
// Порядок выполнения:
// 1. MiddlewareA::handle()
// 2. MiddlewareB::handle()
// 3. MiddlewareC::handle()
// 4. Выполнение экшена роута
// 5. MiddlewareC::terminate()
// 6. MiddlewareB::terminate()
// 7. MiddlewareA::terminate()
Если какой-либо middleware возвращает не-null значение в handle(), цепочка прерывается и ответ возвращается немедленно.
Примеры использования
Пример 1: Защищенное API с CORS
<?php
namespace App\Routes;
use IO\Route;
use IO\CorsTrait;
class ApiRoute extends Route
{
use CorsTrait;
public static function routes($app)
{
$instance = new self();
// Публичное API (без авторизации)
$app->add_route([
"url" => "/api/version",
"name" => "app:api:version",
"method" => "actionVersion",
"cors" => [
'allow_origin' => ['*'],
'allow_methods' => ['GET'],
],
]);
// Защищенное API
$app->add_route([
"url" => "/api/user/data",
"name" => "app:api:userdata",
"method" => "actionUserData",
"cors" => $instance->getCorsConfig('mobi'),
"middleware" => [
\IO\Middleware\AuthMiddleware::class => ['json' => true],
],
]);
}
public function actionVersion()
{
return $this->json(['version' => '1.0.0']);
}
public function actionUserData()
{
global $ioSession;
return $this->json([
'user' => $ioSession->getUserInfo()
]);
}
}
Пример 2: Административная панель с несколькими middleware
<?php
namespace App\Routes;
use IO\Route;
class AdminRoute extends Route
{
public function init()
{
parent::init();
// Добавляем проверку авторизации для всех экшенов
$this->addMiddleware(\IO\Middleware\AuthMiddleware::class, [
'redirect' => '/admin/login',
]);
// Добавляем проверку IP
$this->addMiddleware(\App\Middleware\IpFilterMiddleware::class, [
'whitelist' => ['10.0.0.0/8', '192.168.1.0/24'],
'redirect' => '/403',
]);
}
public function actionIndex()
{
return $this->render('@app/admin/index.twig');
}
public function actionUsers()
{
return $this->render('@app/admin/users.twig');
}
}
Пример 3: Внутренний сервис (без CORS)
// Внутренний сервис, вызывается только через /gate/
$app->add_route([
"url" => "/internal/process",
"name" => "app:internal:process",
"method" => "actionProcess",
"type" => ["post"],
"nosession" => true,
// cors отсутствует - CORS заголовки не добавляются
]);
Пример 4: Динамическое добавление middleware
<?php
namespace App\Routes;
use IO\Route;
class DynamicRoute extends Route
{
public function actionConditional()
{
// Добавляем middleware только при определенном условии
if ($this->input('debug') === 'true')
{
$this->addMiddleware(\App\Middleware\DebugLoggerMiddleware::class);
}
// ... остальной код
}
}
Рекомендации по безопасности
- CORS только там, где нужно - не добавляйте CORS к внутренним сервисам
- Явно указывайте origins - избегайте
*если используются credentials - Ограничивайте методы - разрешайте только необходимые HTTP методы
- Кешируйте preflight - используйте
max_ageдля уменьшения нагрузки - Проверяйте middleware - всегда проверяйте входные данные