Руководство по вебхукам в Laravel

Источник: «The definitive Guide to Webhooks in Laravel»
Освойте вебхуки в Laravel. Узнайте о настройке, безопасности, обработке событий и многом другом, позволяющем создавать мощные интеграции в режиме реального времени.

В наши дни общение в реальном времени не просто ожидается — оно необходимо. Пользователям надоело ждать обновления всей страницы, чтобы получить самую свежую информацию. Появились вебхуки, невоспетые герои мира технологий. О них мало кто говорит, но это скоро изменится. Моя миссия — создать полное руководство по вебхукам в Laravel, сделав его основным ресурсом для интеграции веб-хуков в следующее приложение на Laravel.

Что такое вебхуки

Окунёмся в яркий мир вебхуков! Мне нравится представлять вебхуки как цифровых посланников интернета, позволяющих приложениям передавать данные в режиме реального времени другим приложениям, информируя их о каждом конкретном событии или действии. Будь то событие user registered или payment processed, вебхуки обеспечивают синхронизацию приложений. Они являются ключевым компонентом, обеспечивающим полную синхронизацию других приложений с важными событиями.

Как вебхуки работают

Что за магию творит вебхук? Это как стандартный HTTP-запрос, только с изюминкой! Обычно это HTTP POST-запрос, потому что вебхукам нужно отправлять данные. Но настоящая отличительная особенность: то, что делает вебхуки крутыми, — добавление X-Signature или X-Hub-Signature. Это дополнение гарантирует, что полезная нагрузка не подвергалась вмешательству, сохраняя данные абсолютно чистыми и надёжными!

Повысьте безопасность приложений и защитите данные как никогда раньше с помощью HTTPS! Не позволяйте хитрым злоумышленникам завладеть конфиденциальной информацией, которой делитесь с клиентами. Пришло время обновиться и раскрыть всю мощь HTTPS!

Когда веб-хук появляется в приложении, наступает время шоу! Необходимо проверить, откуда именно он пришёл — обрабатывайте вебхуки только из источников, которые вы знаете и которым доверяете. Как правило, веб-хук приходит с подписанным ключом. Рассматривайте его как секретное рукопожатие. Можно расшифровать X-Signature или X-Hub-Signature, и они должны полностью соответствовать отправленной полезной нагрузке. Если есть какое-то несоответствие между содержимым и расшифрованным заголовком — немедленно остановитесь. Этот вебхук был подделан, поэтому не продолжайте. Давайте следить за безопасностью!

Реализация вебхуков в Laravel

Давайте разберёмся, как эффективно интегрировать вебхуки в приложение Laravel. Готовы? Поехали! 🚀

Шаг 1: Определение маршрута

Route::post(
'ingress/github',
AcceptGitHubWebhooks::class,
)->name('ingress:github');

Настроим маршрут для ingress/github, он станет центром управления всеми вебхуками, пришедшими с GitHub. Его можно настроить в настройках репозитория GitHub в разделе «Webhooks». Когда добавляете этот веб-хук, то можете выбрать заголовок content-type и подпись, оптимизирующую подписание запроса. С этой настройкой всё готово к приёму запросов, поддерживаемых надёжным контроллером для управления всеми действиями. Для дополнительного уровня крутости включите в этот маршрут middleware, чтобы улучшить проверку источника и полезной нагрузки.

Шаг 2: Добавление middleware для верификации

Выполним команду Artisan, создающую middleware, которое улучшит проверку запроса, тщательно проверив источник и содержимое.

php artisan make:middleware VerifyGitHubWebhook

После создания middleware VerifyGitHubWebhook можно обновить определение маршрута.

Route::post(
'ingress/github',
AcceptGitHubWebhooks::class,
)->name('ingress:github')->middleware([
VerifyGitHubWebhook::class,
]);

Давайте перейдём к middleware, чтобы изучить проверку входящего вебхука.

final class VerifyGitHubWebhook
{
public function handle(Request $request, Closure $next): Response
{
if (! $this->isValidSource($request->ip())) {
return new JsonResponse(
data: ['message' => 'Invalid source IP.'],
status: 403,
);
}

$signature = $request->header('X-Hub-Signature-256');
$secret = config('services.github.webhook_secret');

if ( ! $signature || ! $this->isValidSignature(
$request->getContent(),
$signature,
$secret,
)) {
return new JsonResponse(
data: ['message' => 'Invalid signature.'],
status: 403,
);
}

return $next($request);
}

private function isValidSignature(
string $payload,
string $signature,
string $secret,
): bool {
return hash_equals(
'sha256=' . hash_hmac('sha256', $payload, $secret),
$signature
);
}

private function isValidSource(string $ip): bool
{
$validIps = cache()->remember(
key: 'github:webhook_ips',
ttl: 3600,
callback: fn () => Http::get(
url: 'https://api.github.com/meta',
)->json('hooks', []),
);

return in_array($ip, $validIps, true);
}
}

Перейдём непосредственно к middleware. Во-первых, мы получаем IP-адрес источника из запроса и сопоставляем его с IP-адресами, которые получили из API GitHub. Но почему бы не сделать это более эффективным, кэшируя их? Только не забывайте время от времени обновлять кэш. Затем возьмём подпись из заголовка и получим подписывающий ключ из конфигурации приложения. Пора засучить рукава и проверить, что хэшированная версия полезной нагрузки запроса соответствует заданной подписи! Сделаем это, и получим надёжное доказательство того, что GitHub прикрывает нас с данными вебхука, и никто другой не подделывал эти данные.

Шаг 3: Отправка задания в контроллер

Погрузимся в код контроллера, чтобы раскрыть потенциал этого вебхука! Запомните боевой клич: Verify — Queue — Respond! Мы уже проверили источник, так что же дальше? Контроллеру пора отправить фоновое задание с входящей полезной нагрузкой.

final class AcceptGitHubWebhooks
{
public function __construct(private Dispatcher $bus) {}

public function __invoke(Request $request): Response
{
defer(
callback: fn() => $this->bus->dispatch(
command: new HandleGitHubWebhook(
payload: $request->all(),
),
),
);

return new JsonResponse(
data: ['message' => 'Webhook accepted.'],
status: Response::HTTP_ACCEPTED,
);
}
}

Задача контроллера — отправить задание HandleGitHubWebhook в очередь и быстро вернуть ответ, чтобы отправитель был рад узнать, что доставка прошла успешно. На этом этапе ничто не должно помешать рабочему процессу с вебхуками: если столкнётесь с внезапным наплывом вебхуков, очередь будет готова с ними справиться — или можно развернуть дополнительных worker'ов для обработки поставленных в очередь заданий. Мы завернули диспетчеризацию в хелпер Laravel 11, ожидающий, пока ответ не будет отправлен, прежде чем передавать это задание. Я бы назвал это микрооптимизацией, но она довольно эффективна.

Шаг 4: Обработка полезной нагрузки

Контроллер находится в отличном состоянии, но мы не останавливаемся на достигнутом. Нужно решить задачу обработки входящей полезной нагрузки и привести всё в движение. Когда вебхук отправляется к нам, он приходит в виде JSON-объекта, содержащего свойство event. Далее необходимо согласовать название этого события с динамичной внутренней бизнес-логикой. Погрузимся в работу HandleGitHubWebhook и рассмотрим, как это сделать.

final class HandleGitHubWebhook implements ShouldQueue
{
use Queueable;

public function __construct(public array $payload) {}

public function handle(GitHubService $service): void
{
$action = $this->payload['action'];

match ($action) {
'published' => $service->release(
payload: $this->payload,
),
'opened' => $service->opened(
payload: $this->payload,
),
default => Log::info(
message: "Unhandled webhook action: $action",
context: $this->payload,
),
};
}
}

Используем мощь интерфейса ShouldQueue, сигнализируя Laravel, чтобы этот класс получил особое отношение, которого он заслуживает. Далее внедряем трейт Queueable, улучшающий поведение очереди. Конечно, если хочется жить на грани, можно переопределить методы трейта, но, честно говоря, после более чем 8 лет погружения в Laravel мне это никогда не требовалось. Конструктор принимает полезную нагрузку, отправленную из контроллера. Она назначается как публичное свойство, потому что, когда задание сериализуется в очередь, она не может регенерироваться с приватными свойствами без сериализации. Наконец, у нас есть метод handle — важная функция, запускающая работу с заданием в очереди. И знаете что? Можно использовать инъекцию зависимостей для метода handle, если нужно что-то конкретное для управления бизнес-логикой.

Далее необходимо написать сервисный класс, содержащий всю логику обработки вебхуков для каждого источника. Поработаем над сервисом в app/Services/GitHubService.

final class GitHubService
{
public function __construct(private DatabaseManager $database) {}

public function release(array $payload): void
{
$this->database->transaction(function () use ($payload) {
// Здесь обрабатывается логика публикации релиза
});
}

public function opened(array $payload): void
{
$this->database->transaction(function () use ($payload) {
// Здесь обрабатывается логика открытия PR
});
}
}

Определение отдельного метода для каждого события вебхука, которое планируется принять, позволяет поддерживать порядок в сервисном классе. Теперь управление всеми необходимыми вебхуками GitHub позволяет легко расширять их по мере необходимости, не затрагивая основную бизнес-логику.

Обзор

Важно помнить, что хотя мы рассмотрели только вебхуки GitHub, те же принципы можно применить к любому вебхуку. Процесс заключается в перехвате запроса в маршрутизаторе и передаче его в контроллер через middleware. Middleware проверит источник и полезную нагрузку, вернёт приемлемую ошибку, где необходимо, и, наконец, передаст его контроллеру. Задача контроллера — передать данные вебхука в фон как можно быстрее, чтобы не было задержки с ответом на запрос. После отправки ответа можно обработать вебхук в очереди в фоновом режиме, так что основное приложение может принимать любые дополнительные запросы, не влияя на обработку вебхука.

Но есть нюанс! Как убедиться, что получены все вебхуки, которые должны быть получены? Как убедиться, что GitHub повторит отправку вебхуков, если первая попытка по какой-то причине сорвётся? У нас нулевая наблюдаемость, мы не знаем, что может ускользнуть от внимания — или как выглядит соотношение ошибок. Мы не получаем предупреждений и остаёмся в неизвестности о любых сбоях — и это сильно обостряет моё паучье чутье разработчика!

Комментарии


Дополнительные материалы

Предыдущая Статья

Что такое PSR-6: Руководство по стандартам кэширования PHP

Следующая Статья

Различие между PHP getenv() и $_ENV