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,
]);
}
完整流程如下:
- 创建本地订单并生成唯一
session_id; - 将
session_id、金额、币种和订单关联关系写入数据库; - 创建 Przelewy24 交易并跳转到支付页面;
- Przelewy24 请求公开的
url_status; - webhook 调用
verify(); - 验证通过后更新本地订单;
- 成功页通过独立接口查询本地订单状态。
不要在 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(),只有验证成功后才更新本地订单。
备注:内容仅供参考。
