从 payment gateway POST 重定向后用户被登出
结论
通常不是 Laravel 主动清空了 session,而是浏览器的 SameSite Cookie 策略让跨站 POST 请求没有带上 Laravel 的 session Cookie。
支付网关通过表单 POST 回跳时,如果 session Cookie 采用 Laravel 常见的 SameSite=Lax 配置,浏览器通常不会发送这个 Cookie。Laravel 找不到原来的 session,auth('organ')->check() 就会返回 false,还可能创建新的匿名 session,并覆盖原 Cookie。
把路径加入 $except 只能跳过 CSRF Token 校验,无法改变浏览器是否发送 session Cookie。
为什么 GET 正常而 POST 失败
SameSite=Lax 一般允许浏览器在顶层跨站 GET 导航中发送 Cookie,但不会在跨站 POST 请求中发送 Cookie。
因此,两种请求会有不同的结果:
- 支付网关通过
GET返回时,浏览器会携带 session Cookie,Laravel 可以恢复登录状态。 - 支付网关通过
POST返回时,浏览器可能不会携带 session Cookie,Laravel 会把请求视为新的访客会话。
可以打开浏览器开发者工具,在 Network 面板中检查 /callback 请求的 Cookie 请求头,确认其中是否包含 Laravel 的 session Cookie。同时检查响应中是否有新的 Set-Cookie,因为新建的匿名 session 可能覆盖原来的 Cookie。
推荐方案:支付回调不要依赖用户 session
支付结果应根据订单号、支付流水号和网关签名确认,不能依赖当前登录用户。用户可能关闭页面或换设备支付,网关也可能在用户离线后重试通知。
建议按以下流程处理:
- 创建支付订单时,保存本地订单号及其所属用户。
- 将订单号作为网关允许的商户参数传入。
- 回调接口根据订单号查找订单。
- 验证网关签名、金额、币种和支付状态。
- 以幂等方式更新订单,避免重复回调引发重复处理。
- 处理完成后,使用
303跳转到站内GET结果页。
例如:
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::post('/payment/callback', [PaymentController::class, 'callback'])
->name('payment.callback');
Route::get('/payment/result/{order}', [PaymentController::class, 'result'])
->name('payment.result');
控制器可以按下面的结构处理:
public function callback(Request $request)
{
if (! $this->gateway->verifySignature($request->all())) {
abort(400, 'Invalid payment signature.');
}
$order = Order::where('reference', $request->input('order_id'))
->firstOrFail();
if ((string) $order->amount !== (string) $request->input('amount')) {
abort(400, 'Payment amount mismatch.');
}
if ($request->input('status') === 'success' && ! $order->is_paid) {
$order->update([
'is_paid' => true,
'transaction_id' => $request->input('transaction_id'),
]);
}
return redirect()
->route('payment.result', ['order' => $order->getKey()])
->setStatusCode(303);
}
实际字段名称、签名验证方式和网关响应内容,应以对应支付平台的协议为准。不能仅凭请求中的 status=success 判断支付成功。
避免回调覆盖原来的 session Cookie
如果回调接口不需要 session,最好把它配置为无状态接口,不经过 Laravel 的 web session middleware。否则,当浏览器没有提交原 Cookie 时,StartSession middleware 可能创建并写回一个新的 session。
例如,可以把接口放在 routes/api.php:
use Illuminate\Support\Facades\Route;
Route::post('/payment/callback', [PaymentController::class, 'callback'])
->name('payment.callback');
Laravel 8 默认会为 api.php 中的路由添加 /api 前缀,因此支付网关中的地址也要改为:
https://example.com/api/payment/callback
接口处理完支付结果后,再通过 303 跳转到 web.php 中的结果页。无状态回调不会写入新的 Laravel session Cookie,可以减少原登录 Cookie 被覆盖的风险。
如果必须保留当前 URL,也可以为这条路由单独设置一个无状态 middleware 组。不要直接从整个 web middleware 组中移除 session 支持,否则可能影响其他页面。
如果 callback 中必须直接读取登录状态
如果业务确实要求跨站 POST 回调携带 session Cookie,需要把 Cookie 设置为:
SameSite=None; Secure
在 config/session.php 中确认以下配置:
'secure' => env('SESSION_SECURE_COOKIE', true),
'same_site' => env('SESSION_SAME_SITE', 'none'),
在 .env 中设置:
SESSION_SECURE_COOKIE=true
SESSION_SAME_SITE=none
修改后,需要让部署环境重新加载配置。例如,可以清除旧的配置缓存:
php artisan config:clear
生产环境也可以按照现有部署流程重新生成配置缓存:
php artisan config:cache
还要注意以下问题:
SameSite=None必须配合Secure使用。- 网站和 callback 都必须使用 HTTPS。
- 旧浏览器对
SameSite=None的支持可能不一致。 - Cookie 更容易出现在跨站请求中,因此必须妥善处理 CSRF 和回调验签。
- 如果 callback 使用不同的域名,还要检查
SESSION_DOMAIN、主域名,以及www与非www是否一致。 - 即使可以读取登录状态,支付确认也不能只依赖 session。
CSRF 例外应保持精确
支付平台无法提供 Laravel 生成的 CSRF Token,因此可以把支付回调加入例外列表:
protected $except = [
'payment/callback',
];
如果实际地址包含 /api 前缀,应按照真实请求路径配置:
protected $except = [
'api/payment/callback',
];
加入 CSRF 例外后,Laravel 不再检查该请求是否来自站内页面。因此,必须通过支付平台提供的签名、证书、密钥或服务端查询接口来验证请求的真实性。CSRF 豁免不能代替支付验签。
优先让支付 callback 保持无状态,根据订单号和签名处理支付结果,再跳转到站内 GET 结果页。只有确实需要在跨站 POST 请求中读取用户 session 时,才考虑启用 SameSite=None; Secure。
备注:内容仅供参考。
