PHP项目如何实现防重复提交?

wen PHP项目 2

本文目录导读:

PHP项目如何实现防重复提交?

  1. 基于Token的防重复提交(最常用)
  2. 基于时间戳的防重复提交
  3. 基于数据库的唯一约束
  4. 前端+后端双重验证
  5. 完整示例:综合方案
  6. 推荐实践建议

在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();
}
?>

推荐实践建议

  1. 组合使用多种方法:前端防抖 + 后端Token验证 + 数据库唯一约束
  2. 设置适当的过期时间:Token、锁等设置合理的TTL
  3. 用户友好提示:不仅仅是阻止提交,还要给出明确提示
  4. 考虑分布式环境:使用Redis等分布式锁方案
  5. 记录日志:记录重复提交的尝试,便于排查问题
  6. 避免影响正常用户体验:设置合理的时间间隔(一般2-5秒)

选择哪种方法取决于你的具体场景:

  • 简单表单:Token方案足够
  • 高并发场景:推荐使用Redis分布式锁
  • 严格的业务要求:数据库唯一约束是最可靠的

抱歉,评论功能暂时关闭!