本文目录导读:

在PHP项目中实现用户自定义Webhook(网络钩子)功能,本质上是一个 “事件-通知” 模式,用户配置一个URL,当你的系统发生特定事件时,系统主动向该URL发送HTTP请求(通常是POST)并携带数据。
以下是实现该功能的完整步骤、核心代码示例及注意事项。
核心流程(4步)
- 用户配置:用户在后端输入名称、目标URL、选择触发的事件(如“订单支付成功”),系统将其存入数据库。
- 事件触发:当业务代码执行到关键节点时(如支付回调),触发一个事件。
- 调度执行:事件处理程序查询数据库,找出所有订阅了该事件的活跃Webhook。
- 发送请求:使用cURL或HTTP客户端向每个URL发送POST请求,携带事件数据和签名(用于验证)。
数据库设计
需要一张存放Webhook配置的表,以及一张(可选)存放发送日志的表。
webhooks 表:
CREATE TABLE `webhooks` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`user_id` INT NOT NULL,
`name` VARCHAR(255) NOT NULL,
`url` VARCHAR(500) NOT NULL COMMENT '接收通知的URL',
`events` JSON NOT NULL COMMENT '监听的事件列表 ["order.paid","user.registered"]',
`secret` VARCHAR(64) NOT NULL COMMENT '用于签名验证的密钥',
`is_active` TINYINT(1) DEFAULT 1,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
webhook_logs 表(可选但推荐):
CREATE TABLE `webhook_logs` (
`id` BIGINT AUTO_INCREMENT PRIMARY KEY,
`webhook_id` INT NOT NULL,
`event` VARCHAR(100) NOT NULL,
`request_body` JSON,
`response_code` INT,
`response_body` TEXT,
`status` ENUM('pending','success','failed') DEFAULT 'pending',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
核心发送逻辑(PHP实现)
创建一个服务类来处理Webhook的发送,这是最核心的部分。
<?php
// WebhookService.php
class WebhookService
{
/**
* 触发事件,向所有已订阅的Webhook发送通知
* @param string $event 事件名,如 'order.paid'
* @param array $payload 要发送的数据,如订单详情
*/
public function dispatch(string $event, array $payload): void
{
// 1. 从数据库获取所有活跃且订阅了该事件的Webhook
$webhooks = $this->getActiveWebhooksByEvent($event);
foreach ($webhooks as $webhook) {
// 2. 异步执行(推荐用消息队列),避免阻塞主流程
// 此处为演示,使用同步方式,生产环境建议用 Queue::push()
$this->sendWebhook($webhook, $event, $payload);
}
}
/**
* 发送单个Webhook请求
*/
private function sendWebhook(array $webhook, string $event, array $payload): void
{
$data = [
'event' => $event,
'time' => time(),
'payload' => $payload
];
// 1. 生成签名(用于接收方验证来源)
$data['signature'] = $this->generateSignature($data, $webhook['secret']);
// 2. 使用cURL发送POST请求
$ch = curl_init($webhook['url']);
$jsonData = json_encode($data);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $jsonData,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Content-Length: ' . strlen($jsonData),
'User-Agent: MySystem-Webhook/1.0'
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10, // 超时时间10秒
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_SSL_VERIFYPEER => true, // 生产环境务必开启
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
// 3. 记录日志(写入 webhook_logs 表)
$this->logWebhook(
$webhook['id'],
$event,
$jsonData,
$httpCode,
$curlError ?: $response,
$httpCode >= 200 && $httpCode < 300 ? 'success' : 'failed'
);
}
/**
* 生成 HMAC-SHA256 签名
*/
private function generateSignature(array $data, string $secret): string
{
// 按字典序排序,避免顺序不同导致签名失败
ksort($data);
$payload = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
return hash_hmac('sha256', $payload, $secret);
}
/**
* 从数据库获取Webhook配置(伪代码,需替换为实际DB查询)
*/
private function getActiveWebhooksByEvent(string $event): array
{
// 示例: SELECT * FROM webhooks WHERE is_active = 1 AND JSON_CONTAINS(events, '"'.$event.'"')
// 返回数组
return [];
}
/**
* 记录日志(伪代码)
*/
private function logWebhook($webhookId, $event, $requestBody, $responseCode, $responseBody, $status): void
{
// INSERT INTO webhook_logs ...
}
}
在业务代码中触发
当业务逻辑执行到关键节点时,调用 dispatch 方法。
<?php
// 示例:订单支付成功后的控制器
class OrderController
{
public function paySuccess($orderId)
{
// ... 处理支付成功逻辑 ...
// 触发Webhook事件
$webhookService = new WebhookService();
$webhookService->dispatch('order.paid', [
'order_id' => $orderId,
'status' => 'paid',
'amount' => $order['amount'],
'paid_at' => date('Y-m-d H:i:s')
]);
}
}
接收方验证(接收端示例)
接收Webhook的外部系统可以通过验证签名来确认请求来源。
<?php
// 接收端验证脚本
$secret = '用户配置中填写的密钥'; // 应与发送方存储的一致
// 获取POST数据
$rawBody = file_get_contents('php://input');
$data = json_decode($rawBody, true);
if (!$data || !isset($data['signature'])) {
http_response_code(400);
die('Invalid payload');
}
// 验证签名
$signature = $data['signature'];
unset($data['signature']); // 签名本身不参与验签
ksort($data);
$payload = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$expectedSignature = hash_hmac('sha256', $payload, $secret);
if (!hash_equals($expectedSignature, $signature)) {
http_response_code(401);
die('Signature mismatch');
}
// 签名验证通过,处理业务
http_response_code(200);
echo 'OK';
进阶优化与最佳实践
| 问题 | 解决方案 |
|---|---|
| 阻塞主流程 | 使用消息队列(Redis + Queue(如php-resque),RabbitMQ,Beanstalkd),事件发生时dispatch只负责推入队列,Worker异步发送。 |
| 请求重试 | 发送失败时(超时、非2xx),记录失败次数,通过定时任务(Cron)重试(如每5分钟重试,最多3次)。 |
| 幂等性 | Webhook请求中携带唯一event_id(如UUID),接收方依据此ID去重。 |
| 限流保护 | 同一Webhook连续失败超过阈值(如10次),自动停用该Webhook,并通知用户。 |
| 安全加固 |
|
| 性能测试 | 单个Webhook发送耗时约100-500ms(取决于接收端),若用户配置了100个Webhook,同步发送将阻塞10-50秒,必须使用队列。 |
推荐项目结构(Laravel 为例)
如果你是使用Laravel框架,可以利用其事件系统极大简化开发:
app/
├── Events/
│ └── OrderPaid.php // 事件类
├── Listeners/
│ └── SendWebhookNotification.php // 监听器(查询Webhook并推入队列)
├── Jobs/
│ └── SendWebhookJob.php // 队列任务(实际发送请求)
├── Models/
│ └── Webhook.php // Eloquent模型
└── Http/
└── Controllers/
└── WebhookController.php // 用户配置界面的增删改查
在 EventServiceProvider 中绑定:
protected $listen = [
'App\Events\OrderPaid' => [
'App\Listeners\SendWebhookNotification',
],
];
用户配置界面(前端要点)
- 事件列表:以下拉框或多选框展示所有可监听事件(
order.paid,user.registered等)。 - URL验证:输入时做简单校验(格式、协议必须是
https)。 - 测试功能:提供一个“测试发送”按钮,发送一个固定示例事件到配置的URL,让用户确认能收到。
- 密钥(Secret):系统自动生成,可用于签名验证,用户可在接收端使用该密钥验签。
实现用户自定义Webhook的关键点有三:
- 可靠的事件分发机制:使用消息队列异步发送,避免拖慢主业务流程。
- 安全校验:使用HMAC签名和URL白名单/黑名单防止伪造和SSRF攻击。
- 容错与重试:记录发送日志,对失败的请求进行自动重试,并通知用户。
按照以上步骤,你可以构建一个生产级别的自定义Webhook系统。