PAYATHON 2026

Laravel 8 集成 Przelewy24 时 webhook 返回 302

支付老李

结论

302 Redirecting to /login 并不表示 Przelewy24 插件无法接收外部请求。它说明支付通知进入 Laravel 后,被 auth 中间件、控制器构造器或业务代码重定向到了登录页。

Przelewy24 的 url_status 是服务器之间使用的 webhook。请求不会携带用户的 Laravel session、登录 Cookie 或 CSRF token,因此对应地址必须满足以下条件:

  • 无需登录;
  • 不依赖购物车或浏览器 session;
  • 跳过 CSRF 校验;
  • 直接读取 Przelewy24 发送的 POST 数据;
  • 验证成功后返回 2xx 响应。

url_return 和 url_status 处理的是两套不同的流程。浏览器返回成功页不能代替 webhook,成功页中的 AJAX 也不应调用 webhook 的处理方法。

为什么会返回 302

Laravel 的 Authenticate 中间件发现请求未登录时,通常会把普通 Web 请求重定向到 /login。Przelewy24 请求 url_status 时没有登录状态,所以只要 webhook 路由或控制器使用了 auth,就会返回 302。

除了 web.php 中的路由定义,还需要检查以下位置:

  • 路由是否位于 Route::middleware('auth')->group(...) 中;
  • Payment_Controller 的构造器是否调用了 $this->middleware('auth');
  • 是否有自定义中间件检查用户、购物车或登录状态;
  • order_status() 调用的 $this->checkCart() 是否会访问 Auth::user()、session 或重定向到登录页;
  • 是否有全局中间件把未登录请求重定向到 /login。

把路由写在 Auth::routes() 前面,并不能保证它不受认证保护。真正需要确认的是路由最终绑定了哪些 middleware,而不是路由的书写顺序。

可以用下面的命令查看最终生效的中间件:

php artisan route:list --name=order_status

如果修改路由后行为没有变化,还需要清除路由和配置缓存:

php artisan route:clear
php artisan config:clear
php artisan cache:clear

推荐做法:使用独立的 webhook 控制器

Webhook 不应与依赖用户登录和购物车 session 的支付页面共用控制器逻辑。

// routes/web.php

use App\Http\Controllers\Przelewy24WebhookController;

Route::post('/webhooks/przelewy24', Przelewy24WebhookController::class)
    ->name('webhooks.przelewy24');

其他需要登录的支付路由可以单独放入认证路由组:

Route::middleware(['auth', 'verified'])->group(function () {
    Route::get(
        '/orderPaymentMethods',
        'Payment_Controller@paymentMethods'
    )->name('orderPayments_paymentMethods');

    Route::get(
        '/order-success',
        'Payment_Controller@order_success'
    )->name('order_success');
});

不要把 webhook 放入这个路由组。

排除 CSRF 校验

在 Laravel 8 的 app/Http/Middleware/VerifyCsrfToken.php 中添加 webhook 路径:

<?php

namespace App\Http\Middleware;

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

class VerifyCsrfToken extends Middleware
{
    protected $except = [
        'webhooks/przelewy24',
    ];
}

Przelewy24 无法从你的页面取得 Laravel CSRF token,所以必须为 webhook 排除 CSRF 校验。CSRF 校验失败通常会返回 419,不会重定向到 /login。当前出现的 302 更可能是认证中间件或业务代码介入请求造成的。

也可以在路由上明确移除中间件:

use App\Http\Middleware\Authenticate;
use App\Http\Middleware\VerifyCsrfToken;

Route::post('/webhooks/przelewy24', Przelewy24WebhookController::class)
    ->withoutMiddleware([
        Authenticate::class,
        VerifyCsrfToken::class,
    ])
    ->name('webhooks.przelewy24');

不过,独立控制器配合 $except 通常更容易理解和维护。还要确认应用中没有全局的自定义登录检查,因为 withoutMiddleware() 无法移除真正注册为 global middleware 的组件。

控制器不要读取登录状态或购物车

Webhook 控制器只负责处理通知、验证交易并更新服务器端订单:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Response;
use Przelewy24\Przelewy24;
use Przelewy24\Exceptions\ApiResponseException;

class Przelewy24WebhookController extends Controller
{
    public function __invoke()
    {
        $instance = new Przelewy24([
            'merchant_id' => config('services.przelewy24.merchant_id'),
            'crc'         => config('services.przelewy24.crc'),
            'pos_id'      => config('services.przelewy24.pos_id'),
            'live'        => config('services.przelewy24.live', false),
        ]);

        try {
            $webhook = $instance->handleWebhook();

            $verify = $instance->verify([
                'session_id' => $webhook->sessionId(),
                'order_id'   => $webhook->orderId(),
                'amount'     => $webhook->amount(),
            ]);

            /*
             * 根据 session_id 查找本地订单,并核对:
             * - 订单是否存在
             * - 金额是否一致
             * - 币种是否一致
             * - 订单是否已经处理
             *
             * 验证通过后再将订单标记为已支付。
             */

            return response()->json([
                'ok' => true,
            ], Response::HTTP_OK);
        } catch (ApiResponseException $e) {
            report($e);

            return response()->json([
                'ok' => false,
            ], Response::HTTP_BAD_REQUEST);
        } catch (\Throwable $e) {
            report($e);

            return response()->json([
                'ok' => false,
            ], Response::HTTP_INTERNAL_SERVER_ERROR);
        }
    }
}

具体的命名空间和异常类应以项目实际安装的 mnastalski/przelewy24-php 版本为准。

原代码中的这一行应从 webhook 中删除:

$this->checkCart();

服务器回调没有当前用户的购物车,也没有原浏览器的 session。即使路由没有明确绑定 auth,只要 checkCart() 检查登录状态或执行 redirect('/login'),仍然会产生 302。

Webhook 中也不要调用:

Auth::user()
session()
redirect()

如果控制器构造器绑定了 auth

假设原控制器中有以下代码:

public function __construct()
{
    $this->middleware('auth');
}

可以暂时排除 webhook 方法:

public function __construct()
{
    $this->middleware('auth')->except([
        'order_status',
    ]);
}

长期来看,独立的 webhook 控制器更稳妥,可以防止以后新增构造器逻辑时再次影响回调。

正确区分 return URL 和 status URL

创建交易时,需要分别提供两个地址:

$this->transaction = $this->create_instance()->transaction([
    'session_id' => $sessionId,
    'url_return' => route('order_success'),
    'url_status' => route('webhooks.przelewy24'),
    'amount'      => $amount,
    'description' => $description,
    'email'       => $email,
    'currency'    => $currency,
]);

两者的用途不同:

  • url_return:支付完成后将用户浏览器带回网站,仅用于展示页面;
  • url_status:接收 Przelewy24 后台发送的支付通知,用于确认交易并更新订单状态。

用户回到 url_return,不代表交易已经验证成功。用户可能关闭或刷新页面,也可以直接访问该地址,因此不能在 order_success() 中直接将订单标记为已支付。

成功页 AJAX 不应调用 webhook

当前代码在成功页中执行:

$.ajax({
    type: "POST",
    url: "{{ route('order_status') }}"
});

这个请求不包含 Przelewy24 webhook 的 POST 数据,所以:

$instance->handleWebhook();

无法取得 sessionId、orderId 和 amount。即使给浏览器请求添加 CSRF token,它也不会变成支付平台发出的通知。

如果成功页需要轮询付款状态,应另建一个只读接口:

Route::middleware('auth')->get(
    '/orders/{order}/payment-status',
    'Payment_Controller@paymentStatus'
)->name('orders.payment-status');

该接口只查询本地数据库中的订单状态:

public function paymentStatus(Order $order)
{
    $this->authorize('view', $order);

    return response()->json([
        'status' => $order->payment_status,
    ]);
}

完整流程如下:

  1. 创建本地订单并生成唯一 session_id;
  2. 将 session_id、金额、币种和订单关联关系写入数据库;
  3. 创建 Przelewy24 交易并跳转到支付页面;
  4. Przelewy24 请求公开的 url_status;
  5. webhook 调用 verify();
  6. 验证通过后更新本地订单;
  7. 成功页通过独立接口查询本地订单状态。

不要在 Blade 中实例化控制器

下面的写法不符合 Laravel 的正常请求流程:

$transaction = new App\Http\Controllers\Payment_Controller();
$create_trans = $transaction->create_transaction();

在 Blade 中直接 new 控制器,会绕过路由、中间件、依赖注入和统一的异常处理。页面每次渲染时还可能重复创建交易。

交易应通过一个需要认证的 POST 路由创建:

Route::middleware(['auth', 'verified'])->post(
    '/payments/przelewy24',
    'Payment_Controller@create_transaction'
)->name('payments.przelewy24.create');

在 Blade 中提交表单:

<form method="POST" action="{{ route('payments.przelewy24.create') }}">
    @csrf

    <button type="submit">
        {{ __('labels.toPayment') }}
    </button>
</form>

控制器创建交易后直接执行重定向:

public function create_transaction()
{
    // 创建本地订单和 Przelewy24 transaction

    return redirect()->away($transaction->redirectUrl());
}

还需要检查的部署条件

Webhook 路由配置完成后,还要确认:

  • route('webhooks.przelewy24') 生成的是公网可以访问的绝对 HTTPS URL;
  • APP_URL 使用了正确的域名和协议;
  • Nginx、Apache、Cloudflare 或反向代理没有改写该路径;
  • HTTPS 请求不会再次跳转到其他域名或路径;
  • 测试环境与 live => false、商户配置和 CRC 相匹配;
  • 返回内容不是 HTML 登录页;
  • 应用日志中可以看到 webhook 请求进入控制器;
  • 交易金额使用插件和 Przelewy24 API 要求的单位,不要自行转换整数和小数;
  • webhook 可能重复到达,因此订单更新必须具备幂等性。

最需要修复的是 url_status:它应指向一个无需登录、不依赖 session 和购物车,并且跳过 CSRF 校验的独立 POST 端点。该端点读取真实通知并调用 verify(),只有验证成功后才更新本地订单。

备注:内容仅供参考。