使用令牌防止双表单提交

时间:2013-03-25 23:43:19

标签: php

我试图通过添加令牌隐藏字段来阻止用户双重提交论坛。

所以这就是我到目前为止所做的事情(在论坛加载之前我有这个代码来创建一个以当前时间为值的令牌。

$token = time();
setcookie('formToken', $token, time() + 3600);

在我的论坛中,我有一个像这样的隐藏输入

<form method="post" action="'.$PHP_SELF.'?action=update">
<input type="hidden" name="token" value="'.$token.'" />
<input type="submit" value="go" />
</form>

现在在我的页面顶部$ action ==“update”我有这个代码

if(isset($_POST)  &&  ($_POST['token'] != $_COOKIE['formToken'])){
    $error_list .= '<li>You can not submit this forum twise.</li>';
} 

如果我点击F5刷新页面,它会再次提交表单而不显示我的错误。

7 个答案:

答案 0 :(得分:10)

我强烈建议你避开你的系统:它没有经过深度测试,你会浪费时间调试它,它不会避免臭名昭着的“再次发送POSTDATA?”浏览器的确认对话框,这让用户感到困惑。

我建议你使用PRG pattern(帖子/重定向/获取),这也是由像phpbb这样的论坛实现的。

  

Post / Redirect / Get(PRG)是一种Web开发设计模式   防止一些重复的表单提交,创建更直观   用户代理(用户)的界面。 PRG实现了书签和   以可预测的方式刷新按钮,不会创建重复   表格提交。

http://upload.wikimedia.org/wikipedia/commons/3/3c/PostRedirectGet_DoubleSubmitSolution.png

您的问题已经解决了,解决方案就在这里,只需要采取行动 - 滚动您自己似乎就像一个解决方案只会浪费您的时间。 :)

答案 1 :(得分:3)

为什么不在成功提交表单时设置会话?

所以$_SESSION['submitted'] = 1;

然后你可以检查它。

或者做

if(isset($_POST['submit'])  &&  ($_POST['token'] != $_COOKIE['formToken'])){
    $error_list .= '<li>You can not submit this forum twice.</li>';
}

答案 2 :(得分:3)

gd1回答不会阻止双击提交或通过复杂的javascript表单代码上的各种jQuery绑定意外双重提交。

双击可能会更快,然后禁用提交按钮,或使用javascript隐藏它,所以这也不是一个完整的答案。

会话令牌将无法工作,因为会话尚未写入,因此可用于第二个进程的可用或更新,这可能只需几毫秒就可以共享相同的会话ID。会话仅在完成第一个过程后存储。

Cookie技术可以是一个答案,只要两个进程都能够以阻塞方式通过cookie进行通信,这可能会导致与上面的会话共享相同的问题。

最好的解决方案是使用服务器的共享内存访问来检查其他进程是否已使用预生成的数据哈希处理数据(订单,付款等),或者使用数据库表阻止select和insert来检查如果已经提交了预生成的哈希值。

答案 3 :(得分:0)

建议1)

成功提交删除cookie(removeTokens)

    function removeToken()
{
    //set formToken cookie val to "" (or any default xxxx) and the past expiry date for it 
    setcookie("formToken", "", time()-3600);
    //try to unset - this is not needed ,we may try it
    unset($_COOKIE['formToken']);
}

即只是在你的页面上if(isset($ _ POST))removeToken();

建议2)

根据Tom Wright在Avoiding form resubmit in php when pressing f5

的建议执行重定向
 header('Location: formsubmitSucess.php');

答案 4 :(得分:0)

我使用这种方式来防止双重表单提交,它已经适用于所有场合。如果您需要其他问题,请告诉我,因为本教程假设您具有数据库和PHP的中级知识。

第1步:在数据库中添加一个字段,如下所示: 将YOUR-TABLE替换为数据库表的名称。

    ALTER TABLE `YOUR-TABLE` ADD `token` VARCHAR(35) NULL DEFAULT NULL AFTER    `creationtoken`, ADD UNIQUE (`token`) ;

您在表单页面上的第2步,将其添加到第一行: 它将创建一个独特的toke,它将与您的查询一起插入到您的数据库表中,以便稍后检查它以确保没有其他类似的提交到您的数据库中,这意味着没有双重表单提交。

    <?php 
    session_start(); 
    date_default_timezone_set('America/Chicago');
    $_SESSION['token'] = md5(session_id() . time());
    ?>

然后在提交按钮之前添加:

    // add this before the submit button
    // this will post the unique token to the processing page.

    <div style="width:100%; color:#C00; font-weight:normal;">Session Token: <?php echo strtolower($_SESSION['token']) ?></div>     

    <input type="hidden" name="token" id="token" value="<?php echo  $_SESSION['token']?>" />
    // add this before the submit button


    <input type="submit" id="submit" name="submit" class="button" value="Submit" />

第3步:现在在您的process.php页面上

    //this is where all of your form processing takes place.

    // this is where you call the database
    // if you need the database file let me know...
    include("../common/databaseclass.php");
    $db= new database();

    //here the token is posted then the database table is checked and 
    //if the form has already been added it will return a 1 and will 
    //cause the query to die and echo the error message. 

    $token = $_POST['token'];
    $query = "SELECT token FROM YOURTABLE WHERE token = '$token' LIMIT 1";
    $result = $db->query($query);
    $num = mysql_num_rows($result);

    if ($num>0) {die('your form has already been submitted, thank you');}


    else {

    $host = "localhost";
    $user = "user";
    $pass = "password";
    $db_name = "database";

    mysql_connect($host,$user,$pass);
    @mysql_select_db($db_name) or die( "Unable to select database");


    // table query      
    $sql1="INSERT INTO YOURTABLE (
    `token`,
    `user`,
    `email`,
    `password`,
    `newaccount`,
    `zipcode`,
    `city`,
    `state`,
    `country`,
    `telephone`,
    `creationip`,
    `createdaccount`
     ) 
     VALUES (
     '$token',
     '$username',
     '$email',
     '$password',
     '$newaccount',
     '$zipcode',
     '$city',
     '$state',
     '$country',
     '$phone',
     '$ipadress',
     '$createdaccount'

       )";

    $db->query($sql1);
    header("location:" http://home.php ");      

    }

答案 5 :(得分:0)

对于同样的问题,我制作了一个代码,用于我自己的东西。它具有PRG模式,可灵活地在同一页面上使用它或使用extern PHP文件进行重定向 - 易于使用且安全,也许这可能对您有帮助。

class unPOSTer {

        private 
            $post = "KEEP_POST";

        public function __construct(string $name = null) {
            if (version_compare(PHP_VERSION, "5.4.0") >= 0) {
                if (session_status() == PHP_SESSION_NONE) {
                    session_start();
                }
            } else {
                if (!$_SESSION) {
                    session_start();
                }
            }
            $this->post = $name;
        }

        public function unPost() {
            if (session_status() !== PHP_SESSION_ACTIVE) {
                session_start();
            } elseif (strcasecmp($_SERVER["REQUEST_METHOD"],"POST") === 0) {
                $_SESSION[$this->post] = $_POST;
                header("Location: " . $_SERVER["PHP_SELF"] . "?" . $_SERVER["QUERY_STRING"]);
                exit;
            } elseif (isset($_SESSION[$this->post])) {
                $_POST = $_SESSION[$this->post];
            }
        }

        public function retrieve($data) {
            if (isset($_SESSION[$this->post])) {
                $posts = @$_SESSION[$this->post][$data];
                if (isset($posts)) {
                    return $posts;
                } else {
                    return null;
                }
            } 
        }

        public function reset() {
            if (isset($_SESSION[$this->post])) {
                unset($_SESSION[$this->post]);
            }
        }
    }

然后像这样使用它:

<?php
    require_once "unPOSTer.class.php";
    $unpost = new unPOSTer();
    $unpost->unPost();
?>
<form action='' method=POST>
    <input type=text name=fname value="<?php echo $unpost->retrieve("fname"); ?>" placeholder="First Name">
    <input type=text name=lname value="<?php echo $unpost->retrieve("lname"); ?>" placeholder="Last Name">
    <input type=submit name=send value=Send>
</form>
<?php echo $unpost->reset(); ?>

配置不多,如果您愿意,可以在发送表单数据的每个页面上执行此操作。 retrieve()方法会将您发送的数据吐出来,以防您可以返回并修复某些内容。请随意在我的GitHub页面上分叉/拉取它。我在那里添加了2个演示。

答案 6 :(得分:0)

我遇到了同样的问题,这是一个简单的解决方法:

if(!empty($_SESSION['form_token']) && time() - $_SESSION['form_token'] < 3){
    $data['message'] = 'try again later';
    return;
}
$_SESSION['form_token'] = time();

在我的情况下,PRG模式没有任何效果,因为表单同时提交了多次,并且代码尚未执行,并且没有保存任何数据可与之进行比较。