本文目录导读:

- Token令牌机制(最常用)
- Session标记法
- PRG模式(Post/Redirect/Get)
- 数据库唯一性约束
- 前端防重复点击
- 完整解决方案示例
- Redis实现(高并发场景)
- 推荐的综合方案
- 使用建议
- 注意事项
我来详细介绍PHP表单重复提交的预防方案:
Token令牌机制(最常用)
基础实现
<?php
session_start();
// 生成令牌
function generateToken() {
if (empty($_SESSION['form_token'])) {
$_SESSION['form_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['form_token'];
}
// 验证令牌
function validateToken($token) {
if (!empty($_SESSION['form_token']) &&
hash_equals($_SESSION['form_token'], $token)) {
// 验证成功后立即销毁令牌
unset($_SESSION['form_token']);
return true;
}
return false;
}
?>
<!-- 表单页面 -->
<form method="POST" action="submit.php">
<input type="hidden" name="token" value="<?php echo generateToken(); ?>">
<input type="text" name="username" required>
<button type="submit">提交</button>
</form>
<?php
// submit.php 处理页面
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 验证令牌
if (!isset($_POST['token']) || !validateToken($_POST['token'])) {
die('表单已提交,请勿重复操作');
}
// 业务处理
// ...
echo '提交成功!';
}
?>
Session标记法
<?php
session_start();
// 检查是否已提交
if (isset($_SESSION['submitted']) && $_SESSION['submitted']) {
die('请勿重复提交表单');
}
// 处理表单
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 标记已提交
$_SESSION['submitted'] = true;
// 处理业务逻辑
// ...
// 处理完毕后清除标记
unset($_SESSION['submitted']);
}
// 页面跳转(PRG模式)
header('Location: success.php');
exit;
?>
PRG模式(Post/Redirect/Get)
<?php
// 表单处理
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 处理数据
$data = $_POST;
// 保存到session,以便成功后显示
$_SESSION['form_result'] = $data;
// 重定向到结果页面
header('Location: result.php');
exit;
}
?>
<!-- result.php -->
<?php
session_start();
if (isset($_SESSION['form_result'])) {
echo '提交成功!';
echo '你提交的内容是:' . $_SESSION['form_result']['username'];
// 清除session数据
unset($_SESSION['form_result']);
}
?>
数据库唯一性约束
<?php
// 创建唯一字段(如订单号)
public function submitOrder($userId, $orderData) {
try {
// 生成唯一订单号
$orderNo = date('YmdHis') . bin2hex(random_bytes(4));
$stmt = $pdo->prepare(
"INSERT INTO orders (order_no, user_id, order_data, created_at)
VALUES (?, ?, ?, NOW())"
);
$stmt->execute([$orderNo, $userId, json_encode($orderData)]);
// 如果插入成功,说明是第一次提交
return $orderNo;
} catch (PDOException $e) {
// 插入失败,可能是重复提交
if ($e->errorInfo[1] === 1062) {
// 唯一键冲突
return false;
}
throw $e;
}
}
?>
前端防重复点击
<!-- JavaScript 防止重复提交 -->
<form onsubmit="return handleSubmit()">
<input type="text" name="username">
<button type="submit" id="submitBtn">提交</button>
</form>
<script>
function handleSubmit() {
var btn = document.getElementById('submitBtn');
btn.disabled = true;
btn.innerHTML = '提交中...';
// 防止表单重复提交
setTimeout(function() {
btn.disabled = false;
btn.innerHTML = '提交';
}, 3000);
return true;
}
</script>
完整解决方案示例
<?php
session_start();
class FormSubmitHandler {
private $tokenKey = 'form_token';
private $submittedKey = 'submitted_forms';
private $expireTime = 3600; // 1小时过期
// 生成一次性令牌
public function getToken() {
if (empty($_SESSION[$this->tokenKey])) {
$token = [
'value' => bin2hex(random_bytes(32)),
'created_at' => time()
];
$_SESSION[$this->tokenKey] = $token;
}
return $_SESSION[$this->tokenKey]['value'];
}
// 验证提交
public function validate($token) {
// 检查令牌是否存在
if (empty($_SESSION[$this->tokenKey])) {
return ['success' => false, 'message' => '表单已过期'];
}
// 检查令牌是否过期
$tokenData = $_SESSION[$this->tokenKey];
if (time() - $tokenData['created_at'] > $this->expireTime) {
unset($_SESSION[$this->tokenKey]);
return ['success' => false, 'message' => '表单已过期'];
}
// 验证令牌值
if (!hash_equals($tokenData['value'], $token)) {
return ['success' => false, 'message' => '无效的令牌'];
}
// 检查是否是重复提交
$formId = $this->getFormId();
$submittedForms = isset($_SESSION[$this->submittedKey])
? $_SESSION[$this->submittedKey] : [];
if (isset($submittedForms[$formId])) {
return ['success' => false, 'message' => '请勿重复提交'];
}
// 标记已提交
$submittedForms[$formId] = time();
$_SESSION[$this->submittedKey] = $submittedForms;
// 清除令牌
unset($_SESSION[$this->tokenKey]);
return ['success' => true];
}
private function getFormId() {
return md5($_SERVER['REQUEST_URI'] . session_id());
}
}
?>
Redis实现(高并发场景)
<?php
class RedisFormHandler {
private $redis;
private $expireTime = 3600;
public function __construct() {
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
}
public function generateToken() {
$token = bin2hex(random_bytes(32));
$this->redis->setEx(
"form_token:{$token}",
$this->expireTime,
'active'
);
return $token;
}
public function validateToken($token) {
$key = "form_token:{$token}";
// 使用Redis的SETNX实现原子性
$result = $this->redis->set($key, 'used', ['NX', 'EX' => 1]);
if ($result) {
// 首次提交
return true;
}
// 如果键已存在,检查是否为已使用的令牌
$used = $this->redis->get($key);
if ($used === 'used') {
return false; // 重复提交
}
return false;
}
}
?>
推荐的综合方案
<?php
session_start();
class AntiDuplicateSubmit {
private $tokenKey = 'csrf_token';
// 生成CSRF令牌(带时间戳)
public function generateToken() {
if (empty($_SESSION[$this->tokenKey])) {
$token = [
'value' => bin2hex(random_bytes(32)),
'timestamp' => time(),
'used' => false
];
$_SESSION[$this->tokenKey] = $token;
}
return $_SESSION[$this->tokenKey]['value'];
}
// 验证并返回错误信息
public function validate() {
try {
// 1. 检查请求方法
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
throw new Exception('非法请求');
}
// 2. 检查令牌
if (empty($_POST['_token'])) {
throw new Exception('缺少令牌');
}
// 3. 验证令牌
$token = $_POST['_token'];
if (empty($_SESSION[$this->tokenKey]) ||
$_SESSION[$this->tokenKey]['value'] !== $token) {
throw new Exception('无效的令牌');
}
// 4. 检查超时(15分钟)
if (time() - $_SESSION[$this->tokenKey]['timestamp'] > 900) {
throw new Exception('表单已过期');
}
// 5. 检查重复提交
if ($_SESSION[$this->tokenKey]['used']) {
throw new Exception('请勿重复提交');
}
// 6. 标记为已使用
$_SESSION[$this->tokenKey]['used'] = true;
// 7. 清理和重置
$_SESSION['last_form_time'] = time();
return true;
} catch (Exception $e) {
echo $e->getMessage();
return false;
}
}
// 使用示例
public function handleRequest() {
$handler = new AntiDuplicateSubmit();
if ($handler->validate()) {
// 处理业务逻辑
this->processForm();
// 成功后跳转
header('Location: success.php');
exit;
}
}
// 重置令牌
public function resetToken() {
unset($_SESSION[$this->tokenKey]);
}
}
?>
使用建议
- 简单项目:使用 Session + Token 令牌机制
- 一般项目:PRG模式 + Token令牌
- 高并发项目:Redis存储令牌 + Token
- 金融类项目:令牌 + 数据库唯一约束 + 日志审计
注意事项
- ✅ 令牌必须在服务端生成,不能依赖前端
- ✅ 令牌使用一次后就销毁
- ✅ 设置合理的过期时间(建议15-30分钟)
- ✅ 结合前端按钮禁用和防重复点击
- ✅ 使用 hash_equals() 进行安全比较
- ✅ 处理完成后进行页面跳转(PRG模式)
- ✅ 记录日志用于追踪
这个方案能有效防止大多数重复提交问题,同时保持良好的用户体验。