本文目录导读:

在PHP项目中实现防重复提交有多种方法,以下是几种常见且有效的方案:
基于Token的防重复提交(最常用)
前端实现
<form action="submit.php" method="POST">
<input type="hidden" name="token" value="<?php echo generateToken(); ?>">
<!-- 其他表单字段 -->
<button type="submit">提交</button>
</form>
后端实现
<?php
session_start();
// 生成Token
function generateToken() {
$token = bin2hex(random_bytes(32));
$_SESSION['form_token'] = $token;
$_SESSION['token_time'] = time();
return $token;
}
// 验证Token
function validateToken($token) {
if (!isset($_SESSION['form_token']) || !isset($_SESSION['token_time'])) {
return false;
}
// 检查Token是否匹配
if ($_SESSION['form_token'] !== $token) {
return false;
}
// 可选:检查Token过期时间(例如5分钟)
$max_lifetime = 300; // 5分钟
if (time() - $_SESSION['token_time'] > $max_lifetime) {
return false;
}
// 清除Token,防止重复使用
unset($_SESSION['form_token']);
unset($_SESSION['token_time']);
return true;
}
// 接收表单提交
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = $_POST['token'] ?? '';
if (!validateToken($token)) {
die('无效的提交,请刷新页面重试');
}
// 处理业务逻辑
// ...
}
?>
基于时间戳的防重复提交
<?php
session_start();
// 检查提交间隔
function checkSubmissionInterval($min_interval = 5) {
$current_time = time();
if (isset($_SESSION['last_submit_time'])) {
$elapsed = $current_time - $_SESSION['last_submit_time'];
if ($elapsed < $min_interval) {
return false; // 提交过于频繁
}
}
$_SESSION['last_submit_time'] = $current_time;
return true;
}
// 使用示例
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!checkSubmissionInterval(3)) { // 最小间隔3秒
die('请勿频繁提交');
}
// 处理业务逻辑
}
?>
基于数据库的唯一约束
方案A:使用唯一索引
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(50) UNIQUE, -- 唯一约束
user_id INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
<?php
// 生成唯一订单号
function generateOrderNo($user_id) {
return date('YmdHis') . $user_id . mt_rand(1000, 9999);
}
// 插入订单
function createOrder($order_no, $user_id) {
global $pdo;
try {
$stmt = $pdo->prepare("INSERT INTO orders (order_no, user_id) VALUES (?, ?)");
$stmt->execute([$order_no, $user_id]);
return true;
} catch (PDOException $e) {
// 唯一约束冲突,说明重复提交
if ($e->errorInfo[1] == 1062) {
return false; // 重复提交
}
throw $e;
}
}
?>
方案B:使用数据库锁
<?php
// 使用Redis实现分布式锁
function acquireLock($key, $ttl = 10) {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 尝试获取锁
$lock = $redis->set($key, 1, ['NX', 'EX' => $ttl]);
return $lock;
}
function releaseLock($key) {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->del($key);
}
// 使用示例
$lockKey = 'order:user:' . $user_id;
if (!acquireLock($lockKey)) {
die('正在处理中,请勿重复提交');
}
try {
// 处理业务逻辑
// ...
} finally {
releaseLock($lockKey);
}
?>
前端+后端双重验证
前端JavaScript实现
<script>
let isSubmitting = false;
document.querySelector('form').addEventListener('submit', function(e) {
if (isSubmitting) {
e.preventDefault();
alert('请勿重复提交');
return;
}
isSubmitting = true;
// 可选:禁用提交按钮
document.querySelector('button[type="submit"]').disabled = true;
});
</script>
后端验证
<?php
// 结合多种方法
session_start();
// 1. 生成Token
function generateFormToken() {
$token = bin2hex(random_bytes(32));
$_SESSION['form_token_' . $token] = time();
return $token;
}
// 2. 验证Token
function validateFormToken($token) {
if (!isset($_SESSION['form_token_' . $token])) {
return false;
}
// 检查是否过期(30分钟)
if (time() - $_SESSION['form_token_' . $token] > 1800) {
unset($_SESSION['form_token_' . $token]);
return false;
}
// 删除已使用的Token
unset($_SESSION['form_token_' . $token]);
return true;
}
// 3. 检查提交频率
function checkRateLimit($user_id, $action, $limit = 5, $window = 60) {
$key = "rate_limit:{$user_id}:{$action}";
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$current = $redis->incr($key);
if ($current === 1) {
$redis->expire($key, $window);
}
return $current <= $limit;
}
?>
完整示例:综合方案
<?php
session_start();
class AntiDuplicateSubmit {
private $pdo;
private $redis;
private $lockKey;
public function __construct($pdo, $redis = null) {
$this->pdo = $pdo;
$this->redis = $redis;
}
// 生成唯一标识
public function generateId() {
return bin2hex(random_bytes(16));
}
// 检查是否重复提交
public function checkDuplicate($submitId, $userId) {
// 方法1:检查会话中的提交ID
if (isset($_SESSION['submit_id']) && $_SESSION['submit_id'] === $submitId) {
return true; // 重复提交
}
// 方法2:数据库检查(如果已经存在相同数据)
$stmt = $this->pdo->prepare("SELECT id FROM submissions WHERE submit_id = ?");
$stmt->execute([$submitId]);
if ($stmt->fetch()) {
return true; // 重复提交
}
// 方法3:Redis锁
if ($this->redis) {
$lockKey = "submit:lock:{$userId}";
if ($this->redis->exists($lockKey)) {
return true; // 正在处理中
}
}
return false;
}
// 标记提交
public function markSubmitted($submitId, $userId) {
$_SESSION['submit_id'] = $submitId;
// 插入数据库
$stmt = $this->pdo->prepare("INSERT INTO submissions (submit_id, user_id) VALUES (?, ?)");
$stmt->execute([$submitId, $userId]);
// 设置Redis锁
if ($this->redis) {
$lockKey = "submit:lock:{$userId}";
$this->redis->setex($lockKey, 10, time());
}
}
}
// 使用示例
try {
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$antiDup = new AntiDuplicateSubmit($pdo);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submitId = $_POST['submit_id'] ?? '';
$userId = $_SESSION['user_id'] ?? 0;
if (empty($submitId)) {
throw new Exception('无效的提交');
}
if ($antiDup->checkDuplicate($submitId, $userId)) {
die('请勿重复提交');
}
// 标记为已提交
$antiDup->markSubmitted($submitId, $userId);
// 处理业务逻辑
// ...
echo '提交成功';
}
} catch (Exception $e) {
echo '错误:' . $e->getMessage();
}
?>
推荐实践建议
- 组合使用多种方法:前端防抖 + 后端Token验证 + 数据库唯一约束
- 设置适当的过期时间:Token、锁等设置合理的TTL
- 用户友好提示:不仅仅是阻止提交,还要给出明确提示
- 考虑分布式环境:使用Redis等分布式锁方案
- 记录日志:记录重复提交的尝试,便于排查问题
- 避免影响正常用户体验:设置合理的时间间隔(一般2-5秒)
选择哪种方法取决于你的具体场景:
- 简单表单:Token方案足够
- 高并发场景:推荐使用Redis分布式锁
- 严格的业务要求:数据库唯一约束是最可靠的