PHP表单重复提交预防

wen PHP项目 5

本文目录导读:

PHP表单重复提交预防

  1. Token令牌机制(最常用)
  2. Session标记法
  3. PRG模式(Post/Redirect/Get)
  4. 数据库唯一性约束
  5. 前端防重复点击
  6. 完整解决方案示例
  7. Redis实现(高并发场景)
  8. 推荐的综合方案
  9. 使用建议
  10. 注意事项

我来详细介绍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]);
    }
}
?>

使用建议

  1. 简单项目:使用 Session + Token 令牌机制
  2. 一般项目:PRG模式 + Token令牌
  3. 高并发项目:Redis存储令牌 + Token
  4. 金融类项目:令牌 + 数据库唯一约束 + 日志审计

注意事项

  1. ✅ 令牌必须在服务端生成,不能依赖前端
  2. ✅ 令牌使用一次后就销毁
  3. ✅ 设置合理的过期时间(建议15-30分钟)
  4. ✅ 结合前端按钮禁用和防重复点击
  5. ✅ 使用 hash_equals() 进行安全比较
  6. ✅ 处理完成后进行页面跳转(PRG模式)
  7. ✅ 记录日志用于追踪

这个方案能有效防止大多数重复提交问题,同时保持良好的用户体验。

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