所有文章
Laravel webhook POST 通过 VerifyCsrfToken except 绕过 CSRF,在本地 php artisan serve 上用 hash_equals 验证 getContent 字节。
LaravelPHPwebhook debugginglocal testing

Laravel Webhook:CSRF 豁免、原始 body 与签名验证

Laravel 为每条 web 路由启用 CSRF 保护。Webhook 提供商发送 POST 时不带 session cookie 或 CSRF token,控制器执行前就会返回 419。解决方案:针对 webhook 豁免 CSRF,并用 $request->getContent() 读取原始请求体。

为 webhook 路由豁免 CSRF

将 webhook URI 加入 App\Http\Middleware\VerifyCsrfToken$except 数组(Laravel 11+ 在 bootstrap/app.php 注册 middleware):

protected $except = [
    'webhooks/stripe',
    'webhooks/*',
];

仅豁免 server-to-server 路径,切勿全局关闭 CSRF。

读取原始 body 以验证签名

绕过 CSRF 后读取未改动的字节:

public function handle(Request $request)
{
    $payload = $request->getContent(); // raw string
    $signature = $request->header('Stripe-Signature', '');

    if (!$this->verifySignature($payload, $signature)) {
        return response('', 401);
    }

    $event = json_decode($payload, true, 512, JSON_THROW_ON_ERROR);
    $this->processEvent($event);
    return response('', 200);
}

使用 getContent(),不要用 all()input() — Laravel 辅助方法不会返回提供商签名的精确字节。

在业务逻辑前验证签名

提取提供商请求头,从原始 payload 计算 digest,用 hash_equals 比较。失败返回 401。参见 签名验证指南

配置隧道

  1. 启动 Laravel:php artisan serve --port=8000
  2. 另一终端:npx portpreview 8000
  3. 将隧道 URL + 路径粘贴到提供商控制台。
  4. 配置 TrustProxies 信任隧道的 forwarded 头。
  5. 触发测试事件。

常见陷阱

重复读取 body

json() 后调用 getContent() 为空。只读一次,验证后再解析。

中间件顺序

控制器前的全局 JSON 解析会改变字节。隔离 webhook 路由。

重试与幂等

持久化事件 ID — 见 重试与幂等性模式

延伸阅读

基础阅读localhost 隧道基础本地 webhook 调试。签名机制见签名验证指南。防重复处理见重试与幂等性模式免费开始使用 PortPreview

常见问题

为什么 Laravel 对 webhook POST 返回 419?
VerifyCsrfToken 会拒绝没有 CSRF token 的 POST。将 webhook 路径加入 $except,然后验证签名。
webhook 该用 getContent() 还是 all()?
签名验证用 getContent()。all() 和 input() 返回解析后的数据,不是提供商签名的精确字节。
如何在本地测试 Laravel webhook?
运行 php artisan serve,用 npx portpreview 暴露端口,将隧道 URL 粘贴到控制台并触发测试事件。