本文目录导读:

- 基础 Webhook 接收端
- Webhook 签名验证
- 使用框架处理 Webhook
- 异步处理 Webhook
- Webhook 发送端
- Webhook 事件管理
- 安全最佳实践
- 监控与日志
- 完整示例:GitHub Webhook 集成
- 测试工具
- 最佳实践清单
我来系统地介绍 PHP 在 Webhook 生态中的实践方法,Webhook 本质上是 HTTP 回调,PHP 作为服务端语言非常适合处理这类请求。
基础 Webhook 接收端
最简单的接收示例
<?php
// webhook-receiver.php
// 获取原始请求体
$payload = file_get_contents('php://input');
$data = json_decode($payload, true);
// 记录日志
error_log('收到 Webhook: ' . $payload);
// 验证请求
if (empty($data)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid payload']);
exit;
}
// 处理业务逻辑
try {
// 你的业务处理代码
processWebhook($data);
// 返回成功响应
http_response_code(200);
echo json_encode(['status' => 'success']);
} catch (Exception $e) {
error_log('Webhook 处理失败: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
function processWebhook($data) {
// 处理逻辑
}
?>
Webhook 签名验证
GitHub Webhook 验证示例
<?php
class GitHubWebhookVerifier {
private $secret;
public function __construct($secret) {
$this->secret = $secret;
}
public function verify($payload, $signature) {
if (!$signature) {
return false;
}
// 计算 HMAC 签名
$expected = 'sha256=' . hash_hmac('sha256', $payload, $this->secret);
// 使用 hash_equals 防止时序攻击
return hash_equals($expected, $signature);
}
}
// 使用示例
$verifier = new GitHubWebhookVerifier('your_secret_key');
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? '';
if (!$verifier->verify($payload, $signature)) {
http_response_code(401);
exit('Invalid signature');
}
?>
Stripe Webhook 验证
<?php
require 'vendor/autoload.php';
use Stripe\Webhook;
class StripeWebhookHandler {
public function handle($payload, $sigHeader, $endpointSecret) {
try {
$event = Webhook::constructEvent(
$payload, $sigHeader, $endpointSecret
);
// 处理不同事件类型
switch ($event->type) {
case 'payment_intent.succeeded':
$paymentIntent = $event->data->object;
$this->handlePaymentSuccess($paymentIntent);
break;
case 'payment_intent.payment_failed':
$this->handlePaymentFailed($event->data->object);
break;
default:
echo 'Received unknown event type: ' . $event->type;
}
http_response_code(200);
} catch (\UnexpectedValueException $e) {
http_response_code(400);
exit('Invalid payload');
} catch (\Stripe\Exception\SignatureVerificationException $e) {
http_response_code(400);
exit('Invalid signature');
}
}
}
?>
使用框架处理 Webhook
Laravel 示例
<?php
// routes/api.php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\WebhookController;
Route::post('/webhook/github', [WebhookController::class, 'github']);
Route::post('/webhook/stripe', [WebhookController::class, 'stripe']);
?>
<?php
// app/Http/Controllers/WebhookController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class WebhookController extends Controller
{
public function github(Request $request)
{
// 验证签名
$signature = $request->header('X-Hub-Signature-256');
$payload = $request->getContent();
if (!$this->verifyGithubSignature($payload, $signature)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
// 处理事件
$event = $request->header('X-GitHub-Event');
$data = $request->all();
// 异步处理
dispatch(new ProcessGithubWebhook($event, $data));
return response()->json(['status' => 'received']);
}
public function stripe(Request $request)
{
$payload = $request->getContent();
$sigHeader = $request->header('Stripe-Signature');
try {
$event = \Stripe\Webhook::constructEvent(
$payload,
$sigHeader,
config('services.stripe.webhook_secret')
);
// 处理事件
$this->handleStripeEvent($event);
return response()->json(['status' => 'success']);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
}
}
?>
异步处理 Webhook
使用队列处理
<?php
// 使用 Redis 或 RabbitMQ
class AsyncWebhookProcessor {
private $redis;
public function __construct($redis) {
$this->redis = $redis;
}
public function enqueue($webhookName, $data) {
// 推入队列
$this->redis->lpush('webhook_queue', json_encode([
'name' => $webhookName,
'data' => $data,
'timestamp' => time()
]));
}
public function processQueue() {
// 后台 worker 处理
while ($job = $this->redis->rpop('webhook_queue')) {
$webhook = json_decode($job, true);
$this->processWebhook($webhook['name'], $webhook['data']);
}
}
}
?>
Webhook 发送端
发送 Webhook 到其他服务
<?php
class WebhookSender {
private $httpClient;
public function __construct() {
$this->httpClient = new \GuzzleHttp\Client([
'timeout' => 10,
'verify' => false // 生产环境不要这样
]);
}
public function sendWebhook($url, $event, $data, $secret = null) {
$payload = json_encode([
'event' => $event,
'data' => $data,
'timestamp' => time()
]);
$headers = [
'Content-Type' => 'application/json',
'User-Agent' => 'MyApp-Webhook/1.0'
];
// 如果配置了 secret,添加签名
if ($secret) {
$signature = hash_hmac('sha256', $payload, $secret);
$headers['X-Webhook-Signature'] = $signature;
$headers['X-Webhook-Signature-256'] = 'sha256=' . $signature;
}
try {
$response = $this->httpClient->post($url, [
'headers' => $headers,
'body' => $payload,
'allow_redirects' => true
]);
return $response->getStatusCode() === 200;
} catch (\Exception $e) {
error_log('Webhook 发送失败: ' . $e->getMessage());
// 重试逻辑
$this->retryWebhook($url, $event, $data, $secret);
return false;
}
}
private function retryWebhook($url, $event, $data, $secret, $retries = 3) {
if ($retries > 0) {
sleep(5 * (4 - $retries)); // 退避策略
$this->sendWebhook($url, $event, $data, $secret, $retries - 1);
}
}
}
?>
Webhook 事件管理
事件订阅管理
<?php
class WebhookSubscriptionManager {
private $db;
public function __construct(PDO $db) {
$this->db = $db;
}
public function subscribe($userId, $event, $callbackUrl) {
$stmt = $this->db->prepare(
'INSERT INTO webhook_subscriptions (user_id, event, callback_url)
VALUES (?, ?, ?)'
);
return $stmt->execute([$userId, $event, $callbackUrl]);
}
public function getSubscribers($event) {
$stmt = $this->db->prepare(
'SELECT * FROM webhook_subscriptions WHERE event = ? AND active = 1'
);
$stmt->execute([$event]);
return $stmt->fetchAll();
}
public function trigger($event, $data) {
$subscribers = $this->getSubscribers($event);
$sender = new WebhookSender();
foreach ($subscribers as $sub) {
// 异步发送
$sender->sendWebhook(
$sub['callback_url'],
$event,
$data,
$sub['secret']
);
}
}
}
?>
安全最佳实践
完整的 Webhook 安全防护
<?php
class SecureWebhookHandler {
private $ipWhitelist = [
'192.30.252.0/22', // GitHub
'185.199.108.0/22' // GitHub
];
public function handle($payload, $headers, $secret) {
// 1. IP 验证
if (!$this->checkIp($this->getClientIp())) {
return ['status' => 'error', 'message' => 'IP 不允许'];
}
// 2. 验证请求头
if (!$this->validateHeaders($headers)) {
return ['status' => 'error', 'message' => 'Header 验证失败'];
}
// 3. 签名验证
if (!$this->verifySignature($payload, $headers['X-Signature'] ?? '', $secret)) {
return ['status' => 'error', 'message' => '签名无效'];
}
// 4. 请求频率限制
if (!$this->checkRateLimit()) {
return ['status' => 'error', 'message' => '请求过于频繁'];
}
// 5. 幂等性检查
$requestId = $headers['X-Request-ID'] ?? '';
if ($this->isDuplicateRequest($requestId)) {
return ['status' => 'duplicate', 'message' => '重复请求'];
}
// 6. 业务处理
return $this->processPayload($payload);
}
private function checkIp($ip) {
// 实现 IP 白名单检查
foreach ($this->ipWhitelist as $cidr) {
if ($this->ipInRange($ip, $cidr)) {
return true;
}
}
return false;
}
private function verifySignature($payload, $signature, $secret) {
$expected = hash_hmac('sha256', $payload, $secret);
return hash_equals($expected, $signature);
}
private function isDuplicateRequest($requestId) {
// 使用 Redis 检查重复
$cacheKey = "webhook:request:$requestId";
if ($this->redis->exists($cacheKey)) {
return true;
}
$this->redis->setex($cacheKey, 300, 'seen');
return false;
}
}
?>
监控与日志
Webhook 监控系统
<?php
class WebhookMonitor {
private $logger;
private $metrics;
public function logRequest($id, $url, $status, $duration, $data = []) {
$logEntry = [
'id' => $id,
'url' => $url,
'status' => $status,
'duration' => $duration,
'timestamp' => date('Y-m-d H:i:s'),
'data' => $data
];
// 写入日志
$this->logger->info('Webhook 请求', $logEntry);
// 收集指标
$this->metrics->increment("webhook.status.$status");
$this->metrics->histogram('webhook.duration', $duration);
}
public function alertOnFailure($webhookId, $error) {
// 发送告警
$this->sendAlert([
'type' => 'webhook_failed',
'webhook_id' => $webhookId,
'error' => $error,
'time' => time()
]);
}
}
?>
完整示例:GitHub Webhook 集成
<?php
// github-webhook.php
require 'vendor/autoload.php';
class GitHubWebhookIntegration {
private $config;
public function __construct() {
$this->config = [
'secret' => getenv('GITHUB_WEBHOOK_SECRET'),
'log_file' => '/var/log/webhooks/github.log',
'token' => getenv('GITHUB_TOKEN')
];
}
public function handle() {
$headers = getallheaders();
$payload = file_get_contents('php://input');
$event = $headers['X-GitHub-Event'] ?? '';
$signature = $headers['X-Hub-Signature-256'] ?? '';
$delivery = $headers['X-GitHub-Delivery'] ?? '';
// 1. 验证签名
$valid = $this->verifySignature($payload, $signature);
if (!$valid) {
$this->log('Invalid signature', 'error');
http_response_code(401);
return;
}
// 2. 解析数据
$data = json_decode($payload, true);
// 3. 处理事件
$this->processEvent($event, $data);
// 4. 记录日志
$this->log("Webhook received: $event (delivery: $delivery)", 'info');
// 5. 返回响应
http_response_code(200);
echo json_encode(['status' => 'ok']);
}
private function verifySignature($payload, $signature) {
$expected = 'sha256=' . hash_hmac('sha256', $payload, $this->config['secret']);
return hash_equals($expected, $signature);
}
private function processEvent($event, $data) {
switch ($event) {
case 'push':
$this->handlePush($data);
break;
case 'pull_request':
$this->handlePullRequest($data);
break;
case 'issues':
$this->handleIssue($data);
break;
default:
$this->log("Unhandled event: $event", 'warning');
}
}
private function handlePush($data) {
// 部署逻辑
$branch = $this->getBranchName($data['ref']);
$repo = $data['repository']['full_name'];
$commit = $data['head_commit']['message'] ?? '';
$this->log("Push to $repo $branch: $commit");
// 触发部署
if ($branch === 'production') {
$this->triggerDeployment($repo, $commit);
}
}
private function handlePullRequest($data) {
$action = $data['action'];
$prNumber = $data['number'];
$title = $data['pull_request']['title'];
$this->log("PR #$prNumber: $action - $title");
// 自动合并已批准的 PR
if ($action === 'approved') {
$this->autoMerge($prNumber);
}
}
private function handleIssue($data) {
$action = $data['action'];
$issueNumber = $data['issue']['number'];
$title = $data['issue']['title'];
$this->log("Issue #$issueNumber: $action - $title");
// 对新 issue 自动回应
if ($action === 'opened') {
$this->autoRespondToIssue($issueNumber);
}
}
private function triggerDeployment($repo, $commit) {
// 调用部署 API
$url = "https://api.example.com/deploy";
$client = new \GuzzleHttp\Client();
$client->post($url, [
'json' => [
'repo' => $repo,
'commit' => $commit
]
]);
}
private function log($message, $level = 'info') {
$logLine = sprintf(
"[%s] [%s] %s\n",
date('Y-m-d H:i:s'),
strtoupper($level),
$message
);
file_put_contents($this->config['log_file'], $logLine, FILE_APPEND);
}
}
// 执行
$webhook = new GitHubWebhookIntegration();
$webhook->handle();
?>
测试工具
Webhook 测试客户端
<?php
// webhook-test-client.php
class WebhookTester {
public function sendTestWebhook($url, $event, $data = [], $secret = null) {
$payload = json_encode([
'event' => $event,
'data' => $data,
'test' => true,
'timestamp' => time()
]);
$headers = [
'Content-Type' => 'application/json',
'X-Test-Webhook' => 'true'
];
if ($secret) {
$headers['X-Signature'] = hash_hmac('sha256', $payload, $secret);
}
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => array_map(
function($key, $value) {
return "$key: $value";
},
array_keys($headers),
$headers
),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30
]);
$response = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'status_code' => $statusCode,
'response' => json_decode($response, true)
];
}
}
?>
最佳实践清单
-
安全
- 始终验证签名
- 使用 HTTPS
- 实现 IP 白名单
- 添加请求频率限制
-
可靠性
- 实现重试机制
- 使用消息队列
- 记录所有请求日志
- 设置合理的超时时间
-
性能
- 异步处理耗时操作
- 返回 200 快速响应
- 使用缓存减少数据库查询
-
调试
- 详细的日志记录
- 提供测试端点
- 使用 webhook.site 等工具测试
这样就可以在 PHP 中构建健壮的 Webhook 系统了,根据具体需求,你可以选择相应的组件来构建完整的 Webhook 解决方案。