PAYATHON 2026

在 PHP $_SESSION 中保存 checkout 步骤字段是否合适?

支付老李

结论

三步 checkout 的临时表单数据可以保存在 $_SESSION 中。对于步骤少、数据量不大的结账流程,这是一种常见且合适的实现方式。

建议按以下流程处理数据:

  1. 接收当前步骤提交的 POST 数据。
  2. 验证数据并写入 $_SESSION。
  3. 重定向到下一步。
  4. 显示任意步骤时,从 $_SESSION 读取已有数据并回填表单。

不要通过修改 $_REQUEST 来回填数据。$_REQUEST 用于接收请求输入,不适合充当表单状态存储区。示例中的赋值方向也写反了,应该把验证后的 $_POST 数据写入 Session,而不是把 Session 数据写入 $_REQUEST。

推荐的数据结构

可以把 checkout 数据集中放在一个 Session 键下,避免与其他 Session 数据混在一起:

$_SESSION['checkout'] = [
    'contact' => [],
    'shipping' => [],
    'payment' => [],
];

不必在一开始就初始化所有字段,不过按步骤分组通常更容易维护。

保存当前步骤的数据

每个处理页面都需要先启动 Session:

<?php

declare(strict_types=1);

session_start();

下面以联系方式步骤为例:

<?php

declare(strict_types=1);

session_start();

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    header('Location: contact.php');
    exit;
}

$name = trim($_POST['contact_name'] ?? '');
$email = trim($_POST['contact_email'] ?? '');

$errors = [];

if ($name === '') {
    $errors['contact_name'] = '请填写联系人姓名';
}

if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    $errors['contact_email'] = '请填写有效的电子邮箱地址';
}

if ($errors !== []) {
    $_SESSION['checkout_errors'] = $errors;

    // 即使验证失败,也可以保存用户输入,方便重新显示。
    $_SESSION['checkout']['contact'] = [
        'name'  => $name,
        'email' => $email,
    ];

    header('Location: contact.php');
    exit;
}

$_SESSION['checkout']['contact'] = [
    'name'  => $name,
    'email' => $email,
];

header('Location: shipping.php');
exit;

这种提交后再重定向的处理方式属于 Post/Redirect/Get 模式,可以防止用户刷新页面时重复提交表单。

回填表单字段

显示表单时,先从 Session 中读取已有数据:

<?php

declare(strict_types=1);

session_start();

$contact = $_SESSION['checkout']['contact'] ?? [];

$name = $contact['name'] ?? '';
$email = $contact['email'] ?? '';

再把数据安全地输出到表单中:

<form method="post" action="save-contact.php">
    <label for="contact_name">联系人姓名</label>
    <input
        id="contact_name"
        name="contact_name"
        type="text"
        value="<?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>"
    >

    <label for="contact_email">电子邮箱</label>
    <input
        id="contact_email"
        name="contact_email"
        type="email"
        value="<?= htmlspecialchars($email, ENT_QUOTES, 'UTF-8') ?>"
    >

    <button type="submit">继续</button>
</form>

用户从后续步骤返回这个页面时,之前填写的内容会自动显示出来。

这里不能省略 htmlspecialchars()。Session 中的数据最初仍然来自用户输入,如果直接写入 HTML 属性,可能引发 XSS 风险。

上一步不需要重新提交表单

如果上一步只用于返回前一个步骤,使用普通链接即可:

<a href="contact.php">上一步</a>

前一个页面会从 Session 中读取数据并回填表单。

如果当前步骤的数据还没有保存,同时又需要在用户点击上一步时保留这些数据,那么上一步也要提交当前表单。服务器可以根据按钮值决定跳转方向:

<button type="submit" name="navigation" value="previous">上一步</button>
<button type="submit" name="navigation" value="next">继续</button>

处理请求时,再判断具体的跳转方向:

$navigation = $_POST['navigation'] ?? 'next';

if ($navigation === 'previous') {
    header('Location: contact.php');
    exit;
}

header('Location: payment.php');
exit;

无论向前还是向后跳转,都应该先验证并保存允许保存的字段。

完成订单后清理数据

订单创建成功后,应立即删除临时 checkout 数据,避免用户下次结账时误用旧内容:

unset(
    $_SESSION['checkout'],
    $_SESSION['checkout_errors']
);

如果用户取消了结账流程,也可以提供明确的清理操作。用户只是点击上一步时,不要清空 Session。

需要注意的安全问题

不要直接保存整个 $_POST

下面的写法虽然省事,但不建议使用:

$_SESSION['checkout'] = $_POST;

应该明确列出允许接收的字段,并逐个验证。否则,请求中额外伪造的字段也会被保存下来。

不要在 Session 中保存支付敏感信息

姓名、地址和配送方式等临时数据通常可以放入 Session,但不要保存银行卡号、CVV、安全码或第三方支付凭据。支付信息应该交给符合要求的支付服务商处理,应用只保存服务商提供的 token 或订单标识。

Session 不是最终订单数据库

Session 适合保存尚未完成的 checkout 草稿,不适合永久存储订单。用户完成支付或确认订单后,应该再次校验数据,再将其写入数据库。

添加 CSRF 防护

checkout 表单会改变服务器状态,因此需要加入 CSRF token,并在处理 POST 请求时验证:

$_SESSION['csrf_token'] ??= bin2hex(random_bytes(32));

在表单中携带 token:

<input
    type="hidden"
    name="csrf_token"
    value="<?= htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES, 'UTF-8') ?>"
>

处理请求时验证 token:

$submittedToken = $_POST['csrf_token'] ?? '';

if (!hash_equals($_SESSION['csrf_token'], $submittedToken)) {
    http_response_code(403);
    exit('Invalid CSRF token');
}

数据较多时的替代方案

如果 checkout 流程持续时间较长、需要支持跨设备恢复,或者 Session 中需要保存的数据较多,可以在数据库中创建 checkout draft,并在 Session 中只保存草稿 ID:

$_SESSION['checkout_id'] = $checkoutId;

每一步都把新数据更新到数据库草稿中。这种方式更适合复杂商城。对于普通的三步 checkout,使用结构清晰且经过验证的 $_SESSION['checkout'] 通常已经足够。

备注:内容仅供参考。