Laravel 12 中 Webhook 签名验证需自定义中间件,注册于 $middlewareGroups['api'] 且置于限流中间件前,手动拼接原始 body 与 header 计算 HMAC-SHA256,控制器内二次校验并防重放,推荐使用 spatie/laravel-webhook-client。

在 Laravel 12 中为 Webhook 请求启用签名验证,核心是**自定义中间件 + 明确校验时机 + 严格参数处理**,不能依赖内置 signed 中间件(它专用于 URL 签名,不适用于 Webhook 的 POST body 或 header 验证)。
必须放在路由匹配之后、控制器执行之前
Webhook 签名验证中间件必须注册在 $middlewareGroups['api'] 中,而非全局 $middleware。否则静态资源、健康检查等请求也会被强制验签,导致 401/403。
- 在
app/Http/Kernel.php的$middlewareGroups['api']数组里添加你的中间件类,例如:\App\Http\Middleware\VerifyWebhookSignature::class - 确保它排在
\Illuminate\Routing\Middleware\ThrottleRequests::class之前——未验证的恶意请求不应计入限流配额 - 绝对不要在中间件里调用
$request->user()或任何依赖 session/auth 的方法,认证逻辑应后置于签名验证
签名原文拼接必须与客户端完全一致
验签失败绝大多数源于前后端拼接规则不一致。Laravel 不自动帮你生成或解析签名原文,你得手动构造:
- 从
$request->getContent()读取原始 JSON body(不能用$request->all(),会丢失空值、顺序、嵌套结构) - 若签名放在 header(如
X-Hub-Signature-256),需提取该 header 值;若在 query 参数中,需排除signature字段后再拼 - 按约定方式拼原文:常见的是对 body 字符串直接计算(如 GitHub Webhook),或对排序后的 key-value 对 URL 编码后拼接(如 Stripe)
- 统一使用
hash_hmac('sha256', $payload, config('services.webhook.secret'))计算本地签名
控制器内必须二次确认并差异化响应
中间件只负责拦截非法请求并返回 401,但无法区分“签名错”“过期”“重放”,也无法记录审计日志或触发告警:
- 在控制器方法开头主动调用自定义验证方法,例如:
if (! app(VerifyWebhookSignature::class)->isValid($request)) { Log::warning('Invalid webhook signature'); abort(401); } - 若需支持时间戳防重放(如含
timestamp字段),应在中间件或控制器中检查abs($now - $timestamp) <= 300(5 分钟窗口) - 失败时返回空响应或模糊提示(如
return response('', 401)),禁止暴露密钥、算法、具体错误原因
推荐用 spatie/laravel-webhook-client 简化流程
如果你对接的是 GitHub、Stripe、Shopify 等主流平台,直接使用社区成熟方案更安全可靠:
- 通过
composer require spatie/laravel-webhook-client安装 - 发布配置:
php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-config" - 在
config/webhook-client.php中配置每个 provider 的 secret、signature header 名称和处理类 - 它自动完成 header 提取、body 读取、HMAC 校验、重放防护,并把合法请求推入队列异步处理


















