You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

迁移服务器后Filament应用Stripe Webhook遇419页面过期错误求助

Stripe Webhook 419错误排查请求

将应用迁移至新服务器后,Filament v3应用处理Stripe结账会话Webhook时持续出现问题:每次Stripe发送checkout.session.completed Webhook请求,都会返回HTTP 419错误(页面过期)。服务器访问与错误日志均记录该419错误,但Filament日志无报错信息。

环境配置

  • 服务器:搭载KeyHelp面板的Ubuntu VPS,Apache服务,PHP 8.3
  • SSL:通过KeyHelp管理Let’s Encrypt证书
  • CDN:启用Cloudflare全DNS代理
  • 应用:部署在子域名下的Filament v3

应用通过StripeController.php和自定义路由实现结账会话:用户购买商品完成结账后,Stripe通过Webhook返回结账状态,但Webhook请求始终触发419错误。

相关代码

app/Http/Controllers/StripeController.php

<?php

namespace App\Http\Controllers;

use App\Models\Ecg;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Stripe\Checkout\Session;
use Stripe\Stripe;
use Stripe\Webhook;

class StripeController extends Controller
{
    public function createCheckoutSession(Request $request)
    {
        // Setze den API-Schlüssel über die Config
        Stripe::setApiKey(config('services.stripe.secret'));

        $amount = $request->query('amount');
        $ecgId = $request->query('ecg_id'); // Get the ECG ID from the query parameters

        $session = Session::create([
            'payment_method_types' => ['card'],
            'line_items' => [[
                'price_data' => [
                    'currency' => 'eur',
                    'product_data' => [
                        'name' => 'EKG-Befundung',
                    ],
                    'unit_amount' => $amount * 100, // amount in cents
                ],
                'quantity' => 1,
            ]],
            'mode' => 'payment',
            'success_url' => route('payment.success', ['ecg_id' => $ecgId]),
            'cancel_url' => route('payment.cancel'),
            'metadata' => [
                'ecg_id' => $ecgId, // Store the ECG ID in metadata
            ],
        ]);

        return redirect()->away($session->url);
    }

    public function success(Request $request)
    {
        $ecgId = $request->query('ecg_id');
        return view('payment.success', ['ecg_id' => $ecgId]);
    }

    public function cancel()
    {
        return view('payment.cancel');
    }

    public function handleWebhook(Request $request)
    {
        Stripe::setApiKey(config('services.stripe.secret'));
        $endpointSecret = config('services.stripe.webhook_secret');
        
        $payload = $request->getContent();
        $sigHeader = $request->header('Stripe-Signature');

        try {
            $event = Webhook::constructEvent($payload, $sigHeader, $endpointSecret);
        } catch (\UnexpectedValueException $e) {
            return response()->json(['error' => 'Invalid payload'], 400);
        } catch (\Stripe\Exception\SignatureVerificationException $e) {
            return response()->json(['error' => 'Invalid signature'], 400);
        }

        switch ($event->type) {
            case 'checkout.session.completed':
                $session = $event->data->object;
                $ecgId = $session->metadata->ecg_id;

                $ecg = Ecg::find($ecgId);
                if ($ecg) {
                    $ecg->isPaid = true; // Update the isPaid property
                    $ecg->save();
                }
                break;
            default:
                Log::warning('Unhandled event type: ' . $event->type);
        }

        return response()->json(['status' => 'success']);
    }
}

app/Http/Middleware/VerifyCsrfToken.php

<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
    /**
     * The URIs that should be excluded from CSRF verification.
     *
     * @var array
     */
    protected $except = [
        '/webhook', 
        'webhook', 
        'webhook/*',
    ];
}

routes/web.php

<?php

use App\Http\Controllers\InvoiceController;
use App\Http\Controllers\ReportController;
use App\Http\Controllers\StripeController;
use Illuminate\Support\Facades\Route;

Route::get('/invoices/{id}/download', [InvoiceController::class, 'downloadPdf'])->name('invoices.download');
Route::get('/reports/{id}/download', [ReportController::class, 'downloadReportPdf'])->name('reports.download');

Route::get('/create-checkout-session', [StripeController::class, 'createCheckoutSession'])->name('create.checkout.session');
Route::get('/payment-success', [StripeController::class, 'success'])->name('payment.success');
Route::get('/payment-cancel', [StripeController::class, 'cancel'])->name('payment.cancel');

// Define the POST route for the webhook
Route::post('/webhook', [StripeController::class, 'handleWebhook'])->name('webhook.handle');

.env配置

APP_ENV=production
APP_KEY=xxx
APP_DEBUG=true
APP_TIMEZONE=Europe/Berlin
APP_URL=https://app.domain.de

APP_LOCALE=de
APP_FALLBACK_LOCALE=de
APP_FAKER_LOCALE=de_DE

APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database

STRIPE_SECRET=xxx
STRIPE_WEBHOOK_SECRET=xxx

BCRYPT_ROUNDS=12

LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=

SESSION_DRIVER=cookie
SESSION_LIFETIME=120
SESSION_DOMAIN=.domain.de
SESSION_SAME_SITE=None
SESSION_SECURE_COOKIE=true
# SESSION_PATH=/

BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync

CACHE_STORE=database
CACHE_PREFIX=

已尝试的解决方法

  • 在VerifyCsrfToken.php的$except数组中添加/webhook路由以排除CSRF验证
  • 修改.env中的SESSION_SAME_SITE、SESSION_DRIVER配置,验证Stripe密钥
  • 配置Apache正确处理Cloudflare的X-Forwarded-Proto等转发头
  • 在Cloudflare中创建缓存规则排除/webhook路径
  • 配置Laravel会话和Cookie支持跨域请求(SameSite=None、启用安全Cookie)
  • 设置storage和bootstrap/cache文件夹权限为777
  • 临时禁用KeyHelp防火墙
  • 检查PHP内存限制、执行时间、POST大小等配置
  • 创建并配置自定义TrustProxies中间件以正确识别HTTPS请求

目前问题仍未解决,恳请社区提供技术建议或解决方案。

内容的提问来源于stack exchange,提问作者weiss

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 18:45:05