Middleware
Обзор
Flight поддерживает middleware для маршрутов и групп маршрутов. Middleware — это часть вашего приложения, где код выполняется до (или после) обратного вызова маршрута. Это отличный способ добавить проверки аутентификации API в ваш код или убедиться, что пользователь имеет разрешение на доступ к маршруту.
Понимание
Middleware может значительно упростить ваше приложение. Вместо сложного наследования абстрактных классов или переопределения методов middleware позволяет контролировать маршруты, присваивая им вашу пользовательскую логику приложения. Вы можете думать о middleware как о сэндвиче. У вас хлеб снаружи, а затем слои ингредиентов, такие как салат, помидоры, мясо и сыр. Затем представьте, что каждый запрос похож на укус сэндвича, где вы сначала едите внешние слои и продвигаетесь к центру.
Вот визуализация того, как работает middleware. Затем мы покажем вам практический пример того, как это функционирует.
Запрос пользователя по URL /api ---->
Middleware->before() выполняется ----->
Вызываемый метод, прикреплённый к /api, выполняется, и ответ генерируется ------>
Middleware->after() выполняется ----->
Пользователь получает ответ от сервераА вот практический пример:
Пользователь переходит по URL /dashboard
LoggedInMiddleware->before() выполняется
before() проверяет наличие действительной сессии входа
если да, ничего не делать и продолжить выполнение
если нет, перенаправить пользователя на /login
Вызываемый метод, прикреплённый к /api, выполняется, и ответ генерируется
LoggedInMiddleware->after() ничего не определено, поэтому позволяет выполнению продолжиться
Пользователь получает HTML дашборда от сервераПорядок выполнения
Функции middleware выполняются в порядке их добавления к маршруту. Выполнение аналогично тому, как Slim Framework обрабатывает это.
Методы before() выполняются в порядке добавления, а методы after() — в обратном порядке.
Пример: Middleware1->before(), Middleware2->before(), Middleware2->after(), Middleware1->after().
Базовое использование
Вы можете использовать middleware как любой вызываемый метод, включая анонимную функцию или класс (рекомендуется).
Анонимная функция
Вот простой пример:
Flight::route('/path', function() { echo ' Here I am!'; })->addMiddleware(function() {
echo 'Middleware first!';
});
Flight::start();
// Это выведет "Middleware first! Here I am!"Примечание: При использовании анонимной функции интерпретируется только метод
before(). Вы не можете определить поведениеafter()с анонимным классом.
Использование классов
Middleware можно (и следует) регистрировать как класс. Если вам нужна функциональность "after", вы должны использовать класс.
class MyMiddleware {
public function before($params) {
echo 'Middleware first!';
}
public function after($params) {
echo 'Middleware last!';
}
}
$MyMiddleware = new MyMiddleware();
Flight::route('/path', function() { echo ' Here I am! '; })->addMiddleware($MyMiddleware);
// также ->addMiddleware([ $MyMiddleware, $MyMiddleware2 ]);
Flight::start();
// Это отобразит "Middleware first! Here I am! Middleware last!"Вы также можете просто указать имя класса middleware, и он будет создан экземпляр.
Flight::route('/path', function() { echo ' Here I am! '; })->addMiddleware(MyMiddleware::class); Примечание: Если вы передаёте только имя middleware, оно автоматически будет выполнено контейнером внедрения зависимостей, и middleware будет выполнен с параметрами, которые ему нужны. Если у вас не зарегистрирован контейнер внедрения зависимостей, по умолчанию будет передан экземпляр
flight\Engineв__construct(Engine $app).
Использование маршрутов с параметрами
Если вам нужны параметры из маршрута, они будут переданы в виде единого массива в функцию middleware. (function($params) { ... } или public function before($params) { ... }). Причина в том, что вы можете структурировать параметры в группы, и в некоторых из этих групп параметры могут появляться в другом порядке, что нарушит функцию middleware при обращении к неправильному параметру. Таким образом, вы можете обращаться к ним по имени, а не по позиции.
use flight\Engine;
class RouteSecurityMiddleware {
protected Engine $app;
public function __construct(Engine $app) {
$this->app = $app;
}
public function before(array $params) {
$clientId = $params['clientId'];
// jobId может быть передан или нет
$jobId = $params['jobId'] ?? 0;
// возможно, если нет ID задания, вам не нужно ничего искать.
if($jobId === 0) {
return;
}
// выполнить поиск какого-то рода в вашей базе данных
$isValid = !!$this->app->db()->fetchField("SELECT 1 FROM client_jobs WHERE client_id = ? AND job_id = ?", [ $clientId, $jobId ]);
if($isValid !== true) {
$this->app->halt(400, 'You are blocked, muahahaha!');
}
}
}
// routes.php
$router->group('/client/@clientId/job/@jobId', function(Router $router) {
// Эта группа ниже всё ещё получает middleware родителя
// Но параметры передаются в одном единственном массиве
// в middleware.
$router->group('/job/@jobId', function(Router $router) {
$router->get('', [ JobController::class, 'view' ]);
$router->put('', [ JobController::class, 'update' ]);
$router->delete('', [ JobController::class, 'delete' ]);
// больше маршрутов...
});
}, [ RouteSecurityMiddleware::class ]);Группировка маршрутов с middleware
Вы можете добавить группу маршрутов, и каждый маршрут в этой группе будет иметь одинаковый middleware. Это полезно, если вам нужно сгруппировать множество маршрутов, например, с помощью middleware Auth для проверки API-ключа в заголовке.
// добавлено в конце метода группы
Flight::group('/api', function() {
// Этот "пустой" маршрут на самом деле соответствует /api
Flight::route('', function() { echo 'api'; }, false, 'api');
// Это соответствует /api/users
Flight::route('/users', function() { echo 'users'; }, false, 'users');
// Это соответствует /api/users/1234
Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
}, [ new ApiAuthMiddleware() ]);Если вы хотите применить глобальный middleware ко всем вашим маршрутам, вы можете добавить "пустую" группу:
// добавлено в конце метода группы
Flight::group('', function() {
// Это всё ещё /users
Flight::route('/users', function() { echo 'users'; }, false, 'users');
// И это всё ещё /users/1234
Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
}, [ ApiAuthMiddleware::class ]); // или [ new ApiAuthMiddleware() ], одно и то жеРаспространённые случаи использования
Валидация API-ключа
Если вы хотите защитить маршруты /api, проверяя, что API-ключ правильный, вы можете легко справиться с этим с помощью middleware.
use flight\Engine;
class ApiMiddleware {
protected Engine $app;
public function __construct(Engine $app) {
$this->app = $app;
}
public function before(array $params) {
$authorizationHeader = $this->app->request()->getHeader('Authorization');
$apiKey = str_replace('Bearer ', '', $authorizationHeader);
// выполнить поиск в вашей базе данных для API-ключа
$apiKeyHash = hash('sha256', $apiKey);
$hasValidApiKey = !!$this->db()->fetchField("SELECT 1 FROM api_keys WHERE hash = ? AND valid_date >= NOW()", [ $apiKeyHash ]);
if($hasValidApiKey !== true) {
$this->app->jsonHalt(['error' => 'Invalid API Key']);
}
}
}
// routes.php
$router->group('/api', function(Router $router) {
$router->get('/users', [ ApiController::class, 'getUsers' ]);
$router->get('/companies', [ ApiController::class, 'getCompanies' ]);
// больше маршрутов...
}, [ ApiMiddleware::class ]);Теперь все ваши API-маршруты защищены этим middleware валидации API-ключа, который вы настроили! Если вы добавите больше маршрутов в группу роутера, они мгновенно получат ту же защиту!
Валидация входа в систему
Хотите ли вы защитить некоторые маршруты, чтобы они были доступны только пользователям, которые вошли в систему? Это легко достижимо с помощью middleware!
use flight\Engine;
class LoggedInMiddleware {
protected Engine $app;
public function __construct(Engine $app) {
$this->app = $app;
}
public function before(array $params) {
$session = $this->app->session();
if($session->get('logged_in') !== true) {
$this->app->redirect('/login');
exit;
}
}
}
// routes.php
$router->group('/admin', function(Router $router) {
$router->get('/dashboard', [ DashboardController::class, 'index' ]);
$router->get('/clients', [ ClientController::class, 'index' ]);
// больше маршрутов...
}, [ LoggedInMiddleware::class ]);Валидация параметров маршрута
Хотите ли вы защитить своих пользователей от изменения значений в URL для доступа к данным, к которым они не должны иметь доступ? Это можно решить с помощью middleware!
use flight\Engine;
class RouteSecurityMiddleware {
protected Engine $app;
public function __construct(Engine $app) {
$this->app = $app;
}
public function before(array $params) {
$clientId = $params['clientId'];
$jobId = $params['jobId'];
// выполнить поиск какого-то рода в вашей базе данных
$isValid = !!$this->app->db()->fetchField("SELECT 1 FROM client_jobs WHERE client_id = ? AND job_id = ?", [ $clientId, $jobId ]);
if($isValid !== true) {
$this->app->halt(400, 'You are blocked, muahahaha!');
}
}
}
// routes.php
$router->group('/client/@clientId/job/@jobId', function(Router $router) {
$router->get('', [ JobController::class, 'view' ]);
$router->put('', [ JobController::class, 'update' ]);
$router->delete('', [ JobController::class, 'delete' ]);
// больше маршрутов...
}, [ RouteSecurityMiddleware::class ]);Обработка выполнения middleware
Предположим, у вас есть middleware аутентификации, и вы хотите перенаправить пользователя на страницу входа, если он не аутентифицирован. У вас есть несколько вариантов:
- Вы можете вернуть false из функции middleware, и Flight автоматически вернёт ошибку 403 Forbidden, но без настройки.
- Вы можете перенаправить пользователя на страницу входа с помощью
Flight::redirect(). - Вы можете создать пользовательскую ошибку внутри middleware и остановить выполнение маршрута.
Простой и прямолинейный
Вот простой пример return false; :
class MyMiddleware {
public function before($params) {
$hasUserKey = Flight::session()->exists('user');
if ($hasUserKey === false) {
return false;
}
// поскольку это true, всё просто продолжается
}
}Пример перенаправления
Вот пример перенаправления пользователя на страницу входа:
class MyMiddleware {
public function before($params) {
$hasUserKey = Flight::session()->exists('user');
if ($hasUserKey === false) {
Flight::redirect('/login');
exit;
}
}
}Пример пользовательской ошибки
Предположим, вам нужно выбросить JSON-ошибку, потому что вы строите API. Вы можете сделать это так:
class MyMiddleware {
public function before($params) {
$authorization = Flight::request()->getHeader('Authorization');
if(empty($authorization)) {
Flight::jsonHalt(['error' => 'You must be logged in to access this page.'], 403);
// или
Flight::json(['error' => 'You must be logged in to access this page.'], 403);
exit;
// или
Flight::halt(403, json_encode(['error' => 'You must be logged in to access this page.']);
}
}
}См. также
- Маршрутизация - Как сопоставлять маршруты с контроллерами и рендерить представления.
- Запросы - Понимание того, как обрабатывать входящие запросы.
- Ответы - Как настраивать HTTP-ответы.
- Внедрение зависимостей - Упрощение создания и управления объектами в маршрутах.
- Почему фреймворк? - Понимание преимуществ использования фреймворка вроде Flight.
- Пример стратегии выполнения middleware
Устранение неисправностей
- Если у вас есть перенаправление в middleware, но ваше приложение не перенаправляется, убедитесь, что вы добавили инструкцию
exit;в middleware.
Журнал изменений
- v3.1: Добавлена поддержка middleware.