PAYATHON 2026

从 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

支付结果应根据订单号、支付流水号和网关签名确认,不能依赖当前登录用户。用户可能关闭页面或换设备支付,网关也可能在用户离线后重试通知。

建议按以下流程处理:

  1. 创建支付订单时,保存本地订单号及其所属用户。
  2. 将订单号作为网关允许的商户参数传入。
  3. 回调接口根据订单号查找订单。
  4. 验证网关签名、金额、币种和支付状态。
  5. 以幂等方式更新订单,避免重复回调引发重复处理。
  6. 处理完成后,使用 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,最好把它配置为无状态接口,不经过 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。

备注:内容仅供参考。