首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 徹底解決表單重複提交:Token 模式原理、實作與 CodeIgniter 擴充

     編輯:PHP 相關知識

問題根源

使用者送出表單後按 F5 重整、或瀏覽器前進/後退、或雙擊提交按鈕,瀏覽器會**重新發送相同的 POST 請求**,導致資料庫重複寫入 (訂單、留言、註冊)。

解決方案對比

方案原理優點缺點
JS 禁用按鈕前端鎖定 submit簡單繞過 JS 即失效 (curl、停用 JS)
PRG 模式 (Post-Redirect-Get)處理完 302 導向 GET 頁面徹底解決重整重複需額外頁面;無法防雙擊/重複點擊
Token (單次令牌)表單隱藏欄位帶一次性 Token,Server 驗證後銷毀防重整、防重複點擊、防 CSRF、無狀態可擴充需 Session/快取儲存 Token

Token 模式核心流程

  1. 產生:用戶請求表單頁 → Server 產生隨機 Token → 存入 Session → 置入表單 ``。
  2. 提交:用戶送出 → Server 讀取 `$_POST['token']`。
  3. 驗證:比對 Session 中 Token,成功即刪除 Session Token (一次性)。
  4. 判斷:匹配 → 處理業務;不匹配/已用過 → 拒絕並提示。

純 PHP 完整實作 form_token.php

<?php
session_start();

// ---------- Token 工具函式 ----------
function generate_token(): string
{
    $token = bin2hex(random_bytes(16)); // 32 字元十六進位
    $_SESSION['form_token'] = $token;
    return $token;
}

function validate_token(string $input): bool
{
    if (empty($_SESSION['form_token']) || empty($input)) {
        return false;
    }
    // 使用 hash_equals 防時序攻擊
    if (hash_equals($_SESSION['form_token'], $input)) {
        unset($_SESSION['form_token']); // 一次性銷毀
        return true;
    }
    return false;
}

// ---------- 業務邏輯 ----------
$message = '';
$token = generate_token(); // 預先生成,供首次顯示使用

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (validate_token($_POST['token'] ?? '')) {
        // 真正的業務處理
        $data = htmlspecialchars(trim($_POST['data'] ?? ''), ENT_QUOTES, 'UTF-8');
        if ($data !== '') {
            // PDO 範例
            try {
                $pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'user', 'pass');
                $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
                $stmt = $pdo->prepare('INSERT INTO `logs` (`content`) VALUES (?)');
                $stmt->execute([$data]);
                $message = '
儲存成功!返回
'; } catch (PDOException $e) { $message = '
資料庫錯誤:' . $e->getMessage() . '
'; } } else { $message = '
內容不可為空
'; } } else { $message = '
無效或重複的提交,請重新整理頁面
'; $token = generate_token(); // 重新產生給下一次 } } ?> Token 防重複提交示範

進階:Token 過期時間 (防止表單放太久)

function generate_token(int $ttl = 3600): string
{
    $token = bin2hex(random_bytes(16));
    $_SESSION['form_token'] = $token;
    $_SESSION['form_token_time'] = time();
    $_SESSION['form_token_ttl'] = $ttl;
    return $token;
}

function validate_token(string $input): bool
{
    if (empty($_SESSION['form_token']) || empty($input)) return false;
    if (time() - $_SESSION['form_token_time'] > $_SESSION['form_token_ttl']) {
        unset($_SESSION['form_token'], $_SESSION['form_token_time'], $_SESSION['form_token_ttl']);
        return false; // 逾期
    }
    if (hash_equals($_SESSION['form_token'], $input)) {
        unset($_SESSION['form_token'], $_SESSION['form_token_time'], $_SESSION['form_token_ttl']);
        return true;
    }
    return false;
}

CodeIgniter 3 擴充:MY_Form_validation

將 Token 整合進表單驗證庫,支援 `$this->form_validation->valid_token()` 與 `$this->form_validation->create_token()`。

<?php
class MY_Form_validation extends CI_Form_validation {
    protected $token_key = 'csrf_token';
    protected $token_ttl = 7200; // 2 小時

    public function __construct() {
        parent::__construct();
        $this->CI =& get_instance();
        $this->CI->load->library('session');
    }

    public function create_token(bool $output = false): string
    {
        $token = bin2hex(random_bytes(16));
        $this->CI->session->set_userdata($this->token_key, $token);
        $this->CI->session->set_userdata($this->token_key . '_time', time());
        $html = '';
        if ($output) echo $html;
        return $html;
    }

    public function valid_token(): bool
    {
        $saved = $this->CI->session->userdata($this->token_key);
        $saved_time = $this->CI->session->userdata($this->token_key . '_time');
        $posted = $this->CI->input->post($this->token_key);

        if (empty($saved) || empty($posted)) return false;
        if (time() - $saved_time > $this->token_ttl) return false;

        if (hash_equals($saved, $posted)) {
            $this->CI->session->unset_userdata([$this->token_key, $this->token_key . '_time']);
            return true;
        }
        return false;
    }
}

Controller 使用範例

public function form()
{
    $this->load->library('form_validation');
    if ($this->input->post('submit') && $this->form_validation->valid_token()) {
        // 處理業務
        redirect('success');
    }
    $this->form_validation->create_token(true); // 直接輸出 hidden input
    $this->load->view('my_form');
}

最佳實踐清單

  • ✅ 所有 **狀態改變** 操作 (增/刪/改) 必須帶 Token。
  • ✅ Token 使用 **CSPRNG** (`random_bytes`、`openssl_random_pseudo_bytes`),不可用 `md5(time())`。
  • ✅ 驗證成功**立即銷毀** Token,確保一次性。
  • ✅ 結合 **PRG 模式**:處理完 POST → `header('Location: ...')` → 302 導向 GET 結果頁,徹底免疫 F5 重整。
  • ✅ API 介面改用 **JWT** 或 **Opaque Token** + Redis 儲存,不依賴 Session。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved