PHP 怎么Webhook 生态

wen PHP项目 2

本文目录导读:

PHP 怎么Webhook 生态

  1. 基础 Webhook 接收端
  2. Webhook 签名验证
  3. 使用框架处理 Webhook
  4. 异步处理 Webhook
  5. Webhook 发送端
  6. Webhook 事件管理
  7. 安全最佳实践
  8. 监控与日志
  9. 完整示例:GitHub Webhook 集成
  10. 测试工具
  11. 最佳实践清单

我来系统地介绍 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)
        ];
    }
}
?>

最佳实践清单

  1. 安全

    • 始终验证签名
    • 使用 HTTPS
    • 实现 IP 白名单
    • 添加请求频率限制
  2. 可靠性

    • 实现重试机制
    • 使用消息队列
    • 记录所有请求日志
    • 设置合理的超时时间
  3. 性能

    • 异步处理耗时操作
    • 返回 200 快速响应
    • 使用缓存减少数据库查询
  4. 调试

    • 详细的日志记录
    • 提供测试端点
    • 使用 webhook.site 等工具测试

这样就可以在 PHP 中构建健壮的 Webhook 系统了,根据具体需求,你可以选择相应的组件来构建完整的 Webhook 解决方案。

抱歉,评论功能暂时关闭!