PAYATHON 2026

支付回调 callback.php 中无法清除 PHP sessions

支付小周

明确结论

只有 callback.php 成功打开原来的 session,unset() 和 session_destroy() 才会生效。因此,应在输出任何内容前调用 session_start(),并确认回调请求携带了用户浏览器中的 session Cookie。

支付“回调”通常分为两种情况:

  • 支付平台将用户浏览器重定向到 callback.php。浏览器一般会携带原来的 session Cookie,此时可以清除该用户的 session。
  • 支付平台的服务器直接请求 callback.php。由于请求中没有用户浏览器的 session Cookie,程序无法确定应该打开哪个 session。这种情况下,不应依赖 session,而应根据 order_id 查询数据库并处理订单。session 清理应放在用户浏览器访问的支付结果页中。

在 callback.php 中启动 session

清理 session 前,先在文件开头启动 session:

<?php

session_start();

// 后续支付验证逻辑

session_start() 必须在 echo、print、HTML 或其他响应内容之前执行,否则 PHP 可能无法发送或修改 session Cookie。

如果项目使用自定义 session 名称,回调页面也必须使用相同的名称:

<?php

session_name('MY_SESSION');
session_start();

payment.php 和 callback.php 还要使用相同的 session 配置,包括:

  • session.name
  • Cookie 的 domain
  • Cookie 的 path
  • HTTP/HTTPS 设置
  • 同一个 session 存储后端

例如,如果 Cookie 的路径被限制为 /checkout/,位于 /payment/callback.php 的页面就无法收到这个 Cookie。

只清除指定数据

session 启动后,可以直接删除指定的键:

<?php

session_start();

unset($_SESSION['post-data']['name']);
unset($_SESSION['post-data']['phone']);
unset($_SESSION['post-data']['order_desc']);
unset($_SESSION['order_id']);
unset($_SESSION['shopping_cart']);

session_write_close();

没有必要通过 $_GET['order_desc'] 判断是否删除 session 数据。下面的代码检查的是回调 URL 中是否包含 order_desc 参数,并不是检查 session 中有没有这个字段:

if ($_GET['order_desc']) {
    unset($_SESSION['post-data']['order_desc']);
}

这种写法还可能触发未定义索引警告。要删除该字段,直接调用 unset() 即可。即使字段不存在,也不会报错。

如果删除三个字段后不再需要整个 post-data,可以直接删除父级:

unset($_SESSION['post-data']);

清理购物车等重要数据前,必须先确认支付验证成功,不能只根据回调 URL 中的参数判断支付结果。

销毁整个 session

如果要结束当前用户的全部 session,建议同时完成以下三项操作:

  1. 清空当前请求中的 $_SESSION。
  2. 删除浏览器中的 session Cookie。
  3. 销毁服务器端的 session 数据。
<?php

session_start();

// 必须先完成支付签名校验、订单查询和支付状态验证。

$_SESSION = [];

if (ini_get('session.use_cookies')) {
    $params = session_get_cookie_params();

    setcookie(
        session_name(),
        '',
        [
            'expires'  => time() - 42000,
            'path'     => $params['path'],
            'domain'   => $params['domain'],
            'secure'   => $params['secure'],
            'httponly' => $params['httponly'],
            'samesite' => $params['samesite'] ?? 'Lax',
        ]
    );
}

session_destroy();

session_destroy() 只删除服务器端的 session 数据。它不会自动清空当前请求内存中的 $_SESSION,也不一定会删除浏览器保存的 session Cookie。因此,如果只调用下面这一句,调试时可能会误以为销毁操作没有成功:

session_destroy();

推荐的支付处理结构

更稳妥的做法是把“支付确认”和“浏览器 session 清理”分开处理。

支付平台的服务器回调

服务器回调只根据支付平台返回的交易编号和 order_id 处理订单:

<?php

// 1. 验证支付平台签名或请求真实性。
// 2. 使用服务端保存的 order_id 查询订单。
// 3. 调用支付平台的 inquiry/verify API。
// 4. 验证金额、订单号和支付状态。
// 5. 以幂等方式更新数据库。

$orderId = $_POST['order_id'] ?? $_GET['order_id'] ?? null;

if ($orderId === null) {
    http_response_code(400);
    exit('Missing order_id');
}

// 不依赖 $_SESSION 查找订单。

这里不应尝试销毁用户 session,因为支付平台的服务器通常没有用户的 PHPSESSID Cookie。

用户浏览器的返回页面

用户完成支付后,让浏览器访问单独的结果页面,例如 payment-result.php。该页面可以读取订单的最终状态,并清除当前浏览器对应的 session:

<?php

session_start();

$orderId = $_GET['order_id'] ?? null;

if ($orderId === null) {
    http_response_code(400);
    exit('Missing order_id');
}

// 从数据库读取订单状态。
// 不要直接信任 URL 中声明的支付状态。

$paymentSucceeded = true; // 应替换为数据库查询结果

if ($paymentSucceeded) {
    unset($_SESSION['post-data']);
    unset($_SESSION['order_id']);
    unset($_SESSION['shopping_cart']);

    session_write_close();
}

如果支付接口只提供一个 callback URL,需要先确认它的工作方式,也就是浏览器重定向还是支付平台的服务器通知。可以检查请求中的 Cookie,判断当前请求是否带有 session ID:

<?php

session_start();

$sessionCookieName = session_name();
$hasSessionCookie = isset($_COOKIE[$sessionCookieName]);

不要在日志中输出完整的 Cookie 或 session ID。

支付页面通常位于第三方域名。用户返回本站时,浏览器是否发送 session Cookie,还会受到 Cookie 属性的影响:

<?php

session_set_cookie_params([
    'lifetime' => 0,
    'path' => '/',
    'domain' => '',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);

session_start();

需要注意:

  • secure => true 要求站点使用 HTTPS。
  • 顶层 GET 导航通常可以使用 SameSite=Lax。
  • 如果支付平台通过跨站 POST 将浏览器提交到回调地址,SameSite=Lax 可能导致 session Cookie 无法发送。
  • 如果跨站 POST 确实需要携带 Cookie,通常要使用 SameSite=None; Secure。这会扩大 Cookie 的跨站发送范围,需要结合 CSRF 防护和实际支付流程进行评估。
  • 如果 payment.php 与 callback.php 位于不同的子域名,必须正确设置 Cookie 的 domain。更推荐通过订单数据库衔接流程,而不是扩大 session Cookie 的作用域。

不要通过 URL 传递 session ID

理论上,可以把 session ID 作为参数传给支付平台,再由支付平台传回,然后调用 session_id() 打开指定的 session。但这样会把会话凭证暴露在 URL、浏览器历史记录、访问日志和第三方系统中,容易造成 session 劫持,因此不应采用。

支付请求应传递不可猜测的订单标识或一次性状态值,并将其与订单一起保存在数据库中:

$callbackToken = bin2hex(random_bytes(32));

// 将 token 的哈希值与 order_id 保存到数据库。
// 回调 URL 只携带 token,而不是 PHP session ID。

收到回调中的 token 后,先完成校验并查找对应订单,再验证支付结果。这样即使用户的 session 已经过期,订单仍能得到正确处理。

还需要修正的代码问题

payment.php 中有一个多余的分号:

if ($stmt->execute([...]));

这个分号会直接结束 if 语句,导致后面的代码块不再受条件控制。应改为:

if ($stmt->execute([
    ':name'  => $_SESSION['post-data']['name'],
    ':phone' => $_SESSION['post-data']['phone'],
])) {
    while ($row = $stmt->fetch()) {
        $_SESSION['order_id'] = $row['order_id'];
    }
}

另外,代码先读取真实订单号,随后又把 session 中的订单号改成 3:

$order_id = $_SESSION['order_id'];
$_SESSION['order_id'] = 3;

虽然 $order_id 变量仍保留原来的值,但 session 中的订单号已经发生变化,后续页面很容易读到错误结果。如果这行赋值只是调试代码,应当删除。

支付状态和订单关联必须保存在数据库等服务端持久化存储中。session 只用于保存用户浏览器当前流程中的临时数据。服务器回调负责确认订单,浏览器结果页负责清理该浏览器的 session。

备注:内容仅供参考。