本文目录导读:

- 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模式)
- ✅ 记录日志用于追踪
这个方案能有效防止大多数重复提交问题,同时保持良好的用户体验。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播