Laravel 13 Webhook 处理完全指南
Webhook 是现代应用集成的重要机制。本文将深入探讨 Laravel 13 中 Webhook 处理的各种方法和最佳实践。
接收 Webhook
基础 Webhook 控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <?php
namespace App\Http\Controllers;
use Illuminate\Http\Request; use Illuminate\Support\Facades\Log;
class WebhookController extends Controller { public function handle(Request $request) { $payload = $request->all();
Log::info('Webhook received', $payload);
return response()->json(['status' => 'received'], 200); } }
|
签名验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <?php
namespace App\Http\Middleware;
use Closure; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response;
class VerifyWebhookSignature { public function handle(Request $request, Closure $next, string $secretKey = null): Response { $signature = $request->header('X-Webhook-Signature');
if (!$signature) { abort(401, 'Missing signature'); }
$secret = config("webhooks.secrets.{$secretKey}"); $payload = $request->getContent();
$expectedSignature = hash_hmac('sha256', $payload, $secret);
if (!hash_equals($expectedSignature, $signature)) { abort(401, 'Invalid signature'); }
return $next($request); } }
|
Stripe Webhook
Stripe Webhook 处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| <?php
namespace App\Http\Controllers;
use Illuminate\Http\Request; use Stripe\Webhook; use Stripe\Exception\SignatureVerificationException;
class StripeWebhookController extends Controller { public function handle(Request $request) { $payload = $request->getContent(); $signature = $request->header('Stripe-Signature'); $secret = config('services.stripe.webhook_secret');
try { $event = Webhook::constructEvent( $payload, $signature, $secret ); } catch (SignatureVerificationException $e) { return response()->json(['error' => 'Invalid signature'], 400); }
return match ($event->type) { 'payment_intent.succeeded' => $this->handlePaymentSucceeded($event->data->object), 'payment_intent.payment_failed' => $this->handlePaymentFailed($event->data->object), 'customer.subscription.created' => $this->handleSubscriptionCreated($event->data->object), 'customer.subscription.deleted' => $this->handleSubscriptionDeleted($event->data->object), default => response()->json(['status' => 'ignored']), }; }
protected function handlePaymentSucceeded($paymentIntent) { $order = Order::where('stripe_payment_intent_id', $paymentIntent->id)->first();
if ($order) { $order->update(['status' => 'paid']); event(new PaymentSucceeded($order)); }
return response()->json(['status' => 'processed']); }
protected function handlePaymentFailed($paymentIntent) { $order = Order::where('stripe_payment_intent_id', $paymentIntent->id)->first();
if ($order) { $order->update(['status' => 'failed']); event(new PaymentFailed($order)); }
return response()->json(['status' => 'processed']); } }
|
GitHub Webhook
GitHub Webhook 处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| <?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class GitHubWebhookController extends Controller { public function handle(Request $request) { $signature = $request->header('X-Hub-Signature-256'); $secret = config('services.github.webhook_secret'); $payload = $request->getContent();
$expectedSignature = 'sha256=' . hash_hmac('sha256', $payload, $secret);
if (!hash_equals($expectedSignature, $signature)) { return response()->json(['error' => 'Invalid signature'], 401); }
$event = $request->header('X-GitHub-Event'); $data = $request->json()->all();
return match ($event) { 'push' => $this->handlePush($data), 'pull_request' => $this->handlePullRequest($data), 'issues' => $this->handleIssues($data), 'release' => $this->handleRelease($data), default => response()->json(['status' => 'ignored']), }; }
protected function handlePush(array $data) { $repository = $data['repository']['full_name']; $branch = str_replace('refs/heads/', '', $data['ref']); $commits = $data['commits'];
foreach ($commits as $commit) { dispatch(new ProcessCommit($repository, $branch, $commit)); }
return response()->json(['status' => 'processed']); }
protected function handlePullRequest(array $data) { $action = $data['action']; $pullRequest = $data['pull_request'];
if ($action === 'opened' || $action === 'synchronize') { dispatch(new AnalyzePullRequest($pullRequest)); }
return response()->json(['status' => 'processed']); } }
|
通用 Webhook 服务
Webhook 服务类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| <?php
namespace App\Services;
use Illuminate\Http\Request; use Illuminate\Support\Facades\Log;
class WebhookService { protected array $handlers = [];
public function registerHandler(string $event, callable $handler): self { $this->handlers[$event][] = $handler; return $this; }
public function handle(Request $request, string $source): array { $event = $this->extractEvent($request, $source); $payload = $this->extractPayload($request, $source);
$this->log($source, $event, $payload);
$results = [];
if (isset($this->handlers[$event])) { foreach ($this->handlers[$event] as $handler) { try { $results[] = $handler($payload); } catch (\Exception $e) { Log::error("Webhook handler failed", [ 'source' => $source, 'event' => $event, 'error' => $e->getMessage(), ]); } } }
return [ 'source' => $source, 'event' => $event, 'processed' => count($results), ]; }
protected function extractEvent(Request $request, string $source): string { return match ($source) { 'stripe' => $request->json('type'), 'github' => $request->header('X-GitHub-Event'), 'slack' => $request->json('type'), default => $request->json('event', 'unknown'), }; }
protected function extractPayload(Request $request, string $source): array { return match ($source) { 'stripe' => $request->json('data.object', []), default => $request->json()->all(), }; }
protected function log(string $source, string $event, array $payload): void { Log::channel('webhooks')->info("Webhook received", [ 'source' => $source, 'event' => $event, 'payload' => $payload, 'timestamp' => now()->toIso8601String(), ]); } }
|
发送 Webhook
Webhook 发送服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| <?php
namespace App\Services;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log;
class WebhookSender { protected int $timeout = 30; protected int $retryTimes = 3;
public function send(string $url, array $payload, array $options = []): bool { $secret = $options['secret'] ?? null; $headers = $options['headers'] ?? [];
$body = json_encode($payload);
if ($secret) { $signature = hash_hmac('sha256', $body, $secret); $headers['X-Webhook-Signature'] = $signature; }
$headers['Content-Type'] = 'application/json'; $headers['X-Webhook-Timestamp'] = now()->timestamp;
try { $response = Http::withHeaders($headers) ->timeout($this->timeout) ->retry($this->retryTimes, 1000) ->post($url, $payload);
$success = $response->successful();
$this->log($url, $payload, $response->status(), $success);
return $success; } catch (\Exception $e) { Log::error('Webhook send failed', [ 'url' => $url, 'error' => $e->getMessage(), ]);
return false; } }
public function sendAsync(string $url, array $payload, array $options = []): void { dispatch(new SendWebhookJob($url, $payload, $options)); }
protected function log(string $url, array $payload, int $status, bool $success): void { Log::channel('webhooks')->info('Webhook sent', [ 'url' => $url, 'status' => $status, 'success' => $success, 'payload_size' => strlen(json_encode($payload)), ]); } }
|
Webhook 发送任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <?php
namespace App\Jobs;
use App\Services\WebhookSender; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\InteractsWithQueue;
class SendWebhookJob implements ShouldQueue { use InteractsWithQueue, Queueable;
public int $tries = 3; public array $backoff = [10, 60, 300];
public function __construct( protected string $url, protected array $payload, protected array $options = [] ) {}
public function handle(WebhookSender $sender): void { $success = $sender->send($this->url, $this->payload, $this->options);
if (!$success) { $this->fail(new \Exception('Webhook delivery failed')); } } }
|
Webhook 重试
重试策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| <?php
namespace App\Services;
use App\Models\WebhookAttempt; use Illuminate\Support\Facades\Http;
class WebhookRetryService { protected array $backoff = [60, 300, 900, 3600, 86400];
public function attempt(WebhookAttempt $attempt): bool { $response = Http::withHeaders($attempt->headers) ->timeout(30) ->post($attempt->url, $attempt->payload);
$attempt->update([ 'attempts' => $attempt->attempts + 1, 'last_attempt_at' => now(), 'last_response_status' => $response->status(), 'last_response_body' => $response->body(), ]);
if ($response->successful()) { $attempt->update(['status' => 'completed']); return true; }
if ($attempt->attempts >= count($this->backoff)) { $attempt->update(['status' => 'failed']); return false; }
$nextDelay = $this->backoff[$attempt->attempts - 1]; $attempt->update([ 'status' => 'retrying', 'next_attempt_at' => now()->addSeconds($nextDelay), ]);
return false; }
public function scheduleRetry(WebhookAttempt $attempt): void { if ($attempt->status !== 'retrying') { return; }
dispatch(function () use ($attempt) { $this->attempt($attempt); })->delay($attempt->next_attempt_at); } }
|
Webhook 验证
请求验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| <?php
namespace App\Services;
use Illuminate\Http\Request;
class WebhookValidator { public function validateStripe(Request $request): bool { $payload = $request->getContent(); $signature = $request->header('Stripe-Signature'); $secret = config('services.stripe.webhook_secret');
$timestamp = null; $v1Signature = null;
foreach (explode(',', $signature) as $item) { [$key, $value] = explode('=', $item, 2); if ($key === 't') { $timestamp = $value; } elseif ($key === 'v1') { $v1Signature = $value; } }
if (!$timestamp || !$v1Signature) { return false; }
if (abs(time() - $timestamp) > 300) { return false; }
$signedPayload = "{$timestamp}.{$payload}"; $expectedSignature = hash_hmac('sha256', $signedPayload, $secret);
return hash_equals($expectedSignature, $v1Signature); }
public function validateGeneric(Request $request, string $secret): bool { $signature = $request->header('X-Webhook-Signature'); $payload = $request->getContent();
$expectedSignature = hash_hmac('sha256', $payload, $secret);
return hash_equals($expectedSignature, $signature); } }
|
总结
Laravel 13 的 Webhook 处理提供了:
- 完善的签名验证机制
- 多平台 Webhook 支持
- 通用 Webhook 服务
- 异步发送队列
- 重试策略支持
- 完整的日志记录
掌握 Webhook 处理技巧可以实现高效的应用集成。