本文目录导读:

- 核心思路
- 方案一:使用
POST/REDIRECT/GET模式(最推荐) - 方案二:令牌(TOKEN)机制(防重复提交 + 防 CSRF)
- 方案三:前端禁用按钮(辅助手段)
- 方案四:基于数据库主键或唯一索引
- 方案五:使用分布式锁(Redis)
- 总结与最佳实践
在 PHP 项目中处理表单重复提交是一个常见的需求,主要有以下几种常用且有效的方案,你可以根据项目复杂度选择或组合使用。
核心思路
让同一份表单数据只被有效处理一次,通常是通过令牌机制、重定向或前端限制来实现。
使用 POST/REDIRECT/GET 模式(最推荐)
这是防止刷新页面导致重复提交的基础且必须的做法。
原理:当用户提交表单(POST)并处理成功后,不直接返回 HTML,而是发送一个 302 重定向响应,让浏览器去请求另一个 URL(GET)。
<?php
// process_form.php (处理逻辑)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 1. 处理你的业务逻辑(例如写入数据库)
// ...
// 2. 处理成功后,重定向到成功页面(或原页面)
header('Location: success.php');
exit; // 切记:header之后一定要 exit,防止后续代码继续执行
}
?>
效果:用户刷新成功页面(success.php)时,只是重新请求一个 GET 页面,不会重新提交表单数据。
令牌(TOKEN)机制(防重复提交 + 防 CSRF)
这是最核心、最可靠的处理重复提交的方式,尤其适用于支付、下单等关键操作。
原理:在生成表单时,生成一个一次性的、随机的令牌(Token)并存储到 Session 中,提交时,验证提交的 Token 是否与 Session 中的一致,验证通过后,立即销毁 Session 中的 Token,这样第二次提交时 Token 就会失效。
代码实现
<?php
session_start();
// --- 1. 生成表单时:生成 Token 并存储到 Session ---
function generateFormToken() {
$token = bin2hex(random_bytes(32)); // 生成安全的随机数
$_SESSION['form_token'] = $token;
return $token;
}
// --- 2. 表单处理页面(check_form.php) ---
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 检查 Session 中是否有 Token
if (!isset($_SESSION['form_token'])) {
die('非法请求: Token 不存在');
}
// 检查提交的 Token 是否与 Session 一致
$submittedToken = $_POST['form_token'] ?? '';
$sessionToken = $_SESSION['form_token'];
if ($submittedToken !== $sessionToken) {
die('令牌无效,可能为重复提交或非法请求');
}
// ★ 关键步骤:验证成功后,立即销毁 Token,防止再次使用
unset($_SESSION['form_token']);
// 3. 开始执行业务逻辑(例如插入数据库)
// ...
// 业务成功后,使用 POST/REDIRECT/GET 跳转
header('Location: success.php');
exit;
}
?>
<!-- 页面中的表单 (显示表单时) -->
<form method="POST" action="check_form.php">
<!-- 输出隐藏的 Token -->
<input type="hidden" name="form_token" value="<?php echo generateFormToken(); ?>">
<!-- 其他表单字段 -->
<input type="text" name="username" required>
<button type="submit">提交</button>
</form>
优势:
- 防刷新提交:刷新时,Token 已失效。
- 防后退提交:后退后再次提交,Token 已不在 Session 中。
- 防 CSRF:攻击者不知道 Token,无法伪造提交。
- 防点击多次提交。
注意:如果你的业务逻辑比较复杂(例如支付回调),需要配合数据库或 Redis 来持久化 Token。
前端禁用按钮(辅助手段)
只能限制用户的正常操作,无法防止恶意请求(如使用 Postman 提交),所以不能单独依赖。
<!-- 结合 JS 禁用 -->
<script>
document.querySelector('form').addEventListener('submit', function(e) {
const submitBtn = this.querySelector('button[type="submit"]');
submitBtn.disabled = true;
submitBtn.innerText = '提交中...';
// 注意:不要在这里阻止表单提交 (e.preventDefault)
});
</script>
注意:有些人可能用 setTimeout 恢复按钮,但更稳妥的是:提交后永久禁用。
基于数据库主键或唯一索引
原理:在数据库表中,对关键字段(如订单号、请求的唯一标识)设置 UNIQUE 约束,第二次插入相同数据时,数据库会报错,代码里捕获该异常即可。
ALTER TABLE orders ADD UNIQUE KEY `unique_order_no` (`order_no`);
<?php
$orderNo = generateUniqueOrderNo(); // 保证唯一
try {
$stmt = $pdo->prepare("INSERT INTO orders (order_no, ...) VALUES (?, ...)");
$stmt->execute([$orderNo, ...]);
} catch (PDOException $e) {
if ($e->errorInfo[1] == 1062) { // MySQL 重复键错误码
echo '订单已存在,请勿重复提交。';
} else {
throw $e;
}
}
?>
适用场景:支付回调、唯一流水号提交等。
使用分布式锁(Redis)
适用于高并发、微服务场景,防止多个请求同时执行同一条业务逻辑。
实现:使用 Redis 的 SET NX EX 命令(只有不存在时才设置,并设置过期时间)
<?php
$key = 'submit_lock:' . md5($_POST['order_id']); // 唯一标识
$lock = $redis->set($key, 1, ['NX', 'EX' => 10]); // 10 秒后自动解锁
if (!$lock) {
die('请勿重复提交');
}
// 执行业务逻辑...
// 处理完成后,删除锁
$redis->del($key);
?>
注意:需要处理异常情况,比如业务报错时要释放锁。
总结与最佳实践
| 方案 | 推荐度 | 防重复提交 | 防CSRF | 防恶意请求 | 实现难度 |
|---|---|---|---|---|---|
| POST/REDIRECT/GET | 必须 | 防刷新 | 一般 | 一般 | 低 |
| Token 机制 | 强烈推荐 | 中 | |||
| 前端禁用按钮 | 辅助 | 只防普通用户 | 无 | 无 | 低 |
| 数据库唯一索引 | 兜底 | 无 | 低 | ||
| Redis 分布式锁 | 高并发 | 一般 | 中高 |
推荐组合(适用于绝大多数 Web 应用):
- 生成表单时:开启
POST/REDIRECT/GET,结合 Token 机制。 - 前端:增加禁用按钮等友好提示。
- 后端:对关键数据(如订单号)增加数据库唯一索引作为保险。