如何在页面刷新(F5 / CTRL+R)时防止表单重新提交

241

我有一个简单的表单,可以将文本提交到我的SQL表中。问题是,在用户提交文本后,他们可以刷新页面,数据就会再次提交,而不必再次填写表单。我可以在文本提交后将用户重定向到另一个页面,但我希望用户留在同一页。

我记得读过一些关于为每个用户提供唯一会话ID并将其与另一个值进行比较以解决我的问题的内容,但我忘记了它在哪里。


11
Post/Redirect/Get 是一种常见的网页设计模式,用于防止表单重复提交。它由三个步骤组成:
  1. 用户提交表单并使用 POST 方法发送数据。
  2. 服务器处理请求,将结果重定向到另一个页面,使用 GET 方法传递数据。
  3. 浏览器获取重定向后的页面。
这种模式确保了表单只被提交一次,并且避免了在刷新浏览器时出现重复提交表单的问题。
- Marcel
1
为什么你不想将用户重定向到另一个页面? - Adam
1
@Adam:因为这样做会向服务器发送另一个请求,而服务器将再次从数据库中获取一些数据。但这是浪费资源的,因为我们在处理“POST”请求时已经获取了所有所需的数据。 - Eugen Konkov
2
在PRG模式中,您只需重定向到显示成功消息的页面即可,无需进一步从数据库获取。 - Adam
我使用了您的评论,并使用与表单相同的URL进行了重定向。由于这是一个POST请求,查询字符串中没有任何内容,因此我使用Location: https://domain/same_script_path... - 并且我仍然停留在相同的页面,只是没有表单数据。 - Ricky Levi
显示剩余2条评论
21个回答

292

我还想指出,你可以使用 JavaScript 方法 window.history.replaceState 来防止在刷新和返回按钮上重新提交。

<script>
    if ( window.history.replaceState ) {
        window.history.replaceState( null, null, window.location.href );
    }
</script>

概念证明在这里:https://dtbaker.net/files/prevent-post-resubmit.php(链接已失效)

我仍然建议采用Post/Redirect/Get方法,但这是一种新颖的JS解决方案。


2
谢谢,这对我来说非常完美,只需要创建404页面以避免用户误解。 - tess hsu
1
真是太棒了。谢谢。 - user4906240
3
在Safari浏览器里它不能运行,它改变了href但仍然保持数据以进行提交的请求。 - Vo Thanh Tung
4
我认为,任何人在没有 JavaScript 的浏览器中仍然会遇到相同的问题。也许 https://dev59.com/Qm015IYBdhLWcg3w6QHA#38768140 更加适合。 - amarinediary
3
我认为仅使用客户端代码来操纵网站的安全性和安全方面是不可取的。服务器应该抵制滥用。客户端代码应该只是一个有用的补充。 - Paul
显示剩余5条评论

133

使用Post/Redirect/Get模式。http://en.wikipedia.org/wiki/Post/Redirect/Get

在我的网站上,我将把消息存储在cookie或session中,在提交后进行重定向,读取cookie/session,然后清除该会话或cookie变量的值。


1
这使得Chrome在需要仅使用POST的业务开发中无法使用! - JWP
46
如果您使用PRG模式,那么实际上是离开了页面,而问题不是在不重定向的情况下如何使事情工作? - Adam
1
我觉得如果你重定向到同一页,那么 $_POST 就会被清除。据我理解,这是期望的效果。至少这是我的期望效果。不过,我认为如果回答能够明确说明这一点,那么它会更好。 - donutguy640
2
不回答问题。不确定为什么它是被接受的答案。 - ICW
3
使用PRG,你会失去上下文,这对我来说是一个糟糕的解决方案。我们不应该这样做。 - Loenix
显示剩余3条评论

34

您可以通过使用会话变量来防止表单重复提交。

首先,您需要在文本框中设置rand(),并在表单页面上设置$_SESSION['rand']

<form action="" method="post">
  <?php
   $rand=rand();
   $_SESSION['rand']=$rand;
  ?>
 <input type="hidden" value="<?php echo $rand; ?>" name="randcheck" />
   Your Form's Other Field 
 <input type="submit" name="submitbtn" value="submit" />
</form>

然后检查$_SESSION['rand']与文本框$_POST['randcheck']的值是否匹配,如下所示:

if(isset($_POST['submitbtn']) && $_POST['randcheck']==$_SESSION['rand'])
{
    // Your code here
}

确保您在使用每个文件时都使用 session_start() 开始会话。


6
我们可以使用<input type="hidden" name="randcheck" id="randcheck" value="<?php echo microtime(); ?>" />来替代。 - Nikolay Bronskiy
1
是的,我们可以使用microtime()和time()来代替rand(),无论哪个函数或变量提供不同的值,我们都可以使用它。但请确保将该值设置为SESSION变量。在这里,SESSION必须与randcheck字段一起检查,以防止重新提交表单。 - Savoo
会话变量在检查会话和邮寄变量是否匹配后应该被清除吗? - denoise
1
@denoise,我相信@Savoo的想法是,在表单数据的$_POST之后,同一页重新加载以重置会话和POST变量。此外,变量的创建应在检查变量是否匹配之后进行。因此,不需要使用unset - Hmerman6006
2
我目前正在使用这种方法,使用令牌。然而,在多标签提交时,这会创建一个问题。由于它会在每个标签上创建新的 rand() 并覆盖会话值,这意味着旧标签打开后将不再有效。有什么建议吗? - NcXNaV
@NcXNaV 你需要将该令牌存储在自己的字段中,例如 $_SESSION['specificform']['rand'] - undefined

30
我使用这个Javascript代码来阻止在表单提交后刷新页面时弹出要求重新提交表单的弹窗。
if ( window.history.replaceState ) {
  window.history.replaceState( null, null, window.location.href );
}

只需将此行代码放置在您的文件页脚处,即可看到神奇效果。


希望有一个版本可以防止客户端禁用,但它很简短、易于操作、快速,并且能够完成所需的功能。 保留历史记录,以便用户可以在不重新发送帖子的情况下导航回去。 - Péter Vértényi
这对我解决了问题。但是这种方法有什么缺点吗? - Haider Ali J Al-Rustem
我将这段代码放在<form>之后,当用户返回页面时,它不会返回到上一个页面,而是返回到POST链中之前的页面。 - David Spector

17

你应该真的使用Post-Redirect-Get模式来处理这个问题,但如果你以某种方式处于PRG不可行的位置(例如表单本身在一个包含文件中,阻止了重定向),你可以哈希一些请求参数以创建基于内容的字符串,然后检查你是否已经发送过它。

//create digest of the form submission:

    $messageIdent = md5($_POST['name'] . $_POST['email'] . $_POST['phone'] . $_POST['comment']);

//and check it against the stored value:

    $sessionMessageIdent = isset($_SESSION['messageIdent'])?$_SESSION['messageIdent']:'';

    if($messageIdent!=$sessionMessageIdent){//if its different:          
        //save the session var:
            $_SESSION['messageIdent'] = $messageIdent;
        //and...
            do_your_thang();
    } else {
        //you've sent this already!
    }

谢谢分享,但似乎这并不起作用。刷新页面确实会重新提交表单。也许我漏掉了什么? - aLearner
刷新页面会重新提交所有内容,但我们只选择处理提交数据,如果它与上次发送的数据不同(我们将其存储在会话变量'messageIdent'中)。您是否在“if messageIdents are different”子句中处理表单提交(即'do_your_thang()'所在位置)? - Moob
感谢您的回复。我最终只是实现了Post/Redirect/Get模式,所以我现在不记得具体是什么让我困扰了。不过还是非常感谢您的回复。 - aLearner
这种方法并不是为了阻止重新提交...而是为了检测重新提交,以便您可以修改代码,以便不执行如果这是一个新的提交,您将要执行的任何操作。换句话说:使用此方法,您可以检测“原始”提交并放置您的数据。如果不是原始的,请不要放置您的数据。可以显示“重复尝试”警告或者显示“确认”信息。 - TheSatinKnight
很遗憾,使用PRG时,当用户从同一会话中打开更多选项卡时,他们可以从第一个选项卡发送Content#1和从第二个选项卡发送Content#2。现在会话仅存储最近发送的内容,因此Content#1可以再次从第一个选项卡发送,Content#2也可以从第二个选项卡发送,以此类推。因此,在会话持续期间,您应该存储不仅是最新内容的哈希值,而且所有内容的哈希值。或者,您可以在容忍期过后或者如果哈希值太多时删除存储的哈希值。 - Csongor Halmai
显示剩余2条评论

16
提交后,重定向到另一个页面:
... process complete....
header('Location: thankyou.php');

您还可以将页面重定向到相同的页面。

如果您正在处理像评论这样的内容,并且希望用户留在同一页上,则可以使用Ajax来处理表单提交。


14

我找到了一个解决方法。您可以通过操作history对象,在处理完POST请求后避免重定向。

因此,您有以下HTML表单:

<form method=POST action='/process.php'>
 <input type=submit value=OK>
</form>

当你在服务器上处理这个表单时,你可以通过设置 Location 头信息来重定向用户到 /the/result/page,如下所示:

$cat process.php
<?php 
     process POST data here
     ... 
     header('Location: /the/result/page');
     exit();
?>

这里输入图片描述

处理完POST的数据后,您会渲染一个小 <script> 和结果页面/the/result/page

<?php 
     process POST data here
     render the <script>         // see below
     render `/the/result/page`   // OK
?>

你应该渲染的<script>

<script>
    window.onload = function() {
        history.replaceState("", "", "/the/result/page");
    }
</script>

结果为:

enter image description here

可以看到表单数据被POSTprocess.php脚本。
此脚本使用以下方式处理POST的数据并立即渲染/the/result/page

  1. 无重定向
  2. 当您刷新页面(F5)时,没有重新POST数据
  3. 当您通过浏览器历史记录导航到上一页/下一页时,不会重新POST

更新

作为另一种解决方案,我请求Mozilla FireFox团队提供一个功能来允许用户设置NextPage头文件,它将像Location头文件一样工作,并使post/redirect/get模式过时。

简而言之。服务器成功处理表单POST数据时,它将:

  1. 设置NextPage头而不是Location
  2. post/redirect/get模式呈现处理POST表单数据的结果,就像呈现GET请求一样

浏览器反过来当看到NextPage头时:

  1. 根据NextPage值调整window.location
  2. 当用户刷新页面时,浏览器将向NextPage协商GET请求而不是重新POST表单数据

如果实现了这一点,那会很棒,对吧? =)


9
  1. 使用 header 函数重定向页面。

    header("Location:your_page.php"); 可以将页面重定向到同一页面或不同页面。

  2. 插入数据到数据库后,使用 unset 函数清除 $_POST 变量。

    unset($_POST);


64
在Chrome浏览器中,取消设置$ _POST根本不会影响表单的重新提交。 - Gavin
2
它行不通。当用户返回页面时,它会再次设置POST变量。 - Sandhu
1
使用unset的原因是什么?请在您的回答中添加一些解释,以便其他人可以从中学习。 - Nico Haase

7
一种非常可靠的方法是在文章中实现唯一标识符并将其缓存。
<input type='hidden' name='post_id' value='".createPassword(64)."'>

然后在你的代码中这样做:

if( ($_SESSION['post_id'] != $_POST['post_id']) )
{
    $_SESSION['post_id'] = $_POST['post_id'];
    //do post stuff
} else {
    //normal display
}

function createPassword($length)
{
    $chars = "abcdefghijkmnopqrstuvwxyz023456789";
    srand((double)microtime()*1000000);
    $i = 0;
    $pass = '' ;

    while ($i <= ($length - 1)) {
        $num = rand() % 33;
        $tmp = substr($chars, $num, 1);
        $pass = $pass . $tmp;
        $i++;
    }
    return $pass;
}

我实际上刚写了类似的东西,但没有费心创建密码,我只是枚举了我的表单(例如:步骤1-5),所以如果它们相等,我们可以继续进行,否则不要保存到数据库或发送电子邮件。但让用户去任何地方。 - Fernando Silva
1
我认为这比重新加载页面更好的解决方案,因为当发布成功时我会显示一条消息,而重新加载页面将删除该消息。这对我非常有效,您也可以使用uniqid。 - Julio Popócatl

3

Moob的帖子的优化版本。创建POST的哈希值,将其保存为会话cookie,并在每个会话中比较哈希值。

// Optionally Disable browser caching on "Back"
header( 'Cache-Control: no-store, no-cache, must-revalidate' );
header( 'Expires: Sun, 1 Jan 2000 12:00:00 GMT' );
header( 'Last-Modified: ' . gmdate('D, d M Y H:i:s') . 'GMT' );

$post_hash = md5( json_encode( $_POST ) );

if( session_start() )
{
    $post_resubmitted = isset( $_SESSION[ 'post_hash' ] ) && $_SESSION[ 'post_hash' ] == $post_hash;
    $_SESSION[ 'post_hash' ] = $post_hash;
    session_write_close();
}
else
{
    $post_resubmitted = false;
}

if ( $post_resubmitted ) {
  // POST was resubmitted
}
else
{
  // POST was submitted normally
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接