在 PHP $_SESSION 中保存 checkout 步骤字段是否合适?
结论
三步 checkout 的临时表单数据可以保存在 $_SESSION 中。对于步骤少、数据量不大的结账流程,这是一种常见且合适的实现方式。
建议按以下流程处理数据:
- 接收当前步骤提交的
POST数据。 - 验证数据并写入
$_SESSION。 - 重定向到下一步。
- 显示任意步骤时,从
$_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'] 通常已经足够。
备注:内容仅供参考。
