在Web系统中,表单重复提交是一个容易被忽视却影响很大的问题。用户快速双击提交按钮、页面卡顿后重试、或者网络延迟导致的重复请求,都会让后端逻辑被执行多次。使用PHP做服务端开发时,仅靠前端禁用按钮并不能彻底解决,因为请求可以被伪造或重发,必须在服务端引入令牌机制来识别每一次合法提交。

为什么需要令牌验证
表单重复提交会带来脏数据。例如用户下单时连续点击两次,如果后端没有防护,可能生成两笔订单。传统的做法是在提交后跳转页面,但ajax异步提交普及后,页面不刷新,用户依然可以重复点。令牌(token)的本质是服务端发放的一次性凭证,每次渲染表单时生成,提交时校验,用完即废。
除了防重复,令牌也能顺带缓解csrf跨站请求伪造。因为攻击者无法获取目标用户会话中的令牌值,也就无法构造合法请求。所以我们常把这种令牌叫做csrf_token,它同时兼顾了两种安全目标。在PHP里,最基础的存储位置就是session,每个用户独立,互不干扰。
PHP后端令牌生成与校验
我们先写一个简洁的令牌管理类,负责生成、写入session、对比和销毁。生成时使用随机字节再转十六进制,保证不可预测。注意session必须在输出前开启,实际框架中通常已自动处理。
<?php
session_start();
class FormToken
{
// 生成并存入session
public static function generate($name = 'form_token')
{
$token = bin2hex(random_bytes(32));
$_SESSION[$name] = $token;
return $token;
}
// 校验并销毁,防止重复使用
public static function check($input, $name = 'form_token')
{
if (empty($_SESSION[$name])) {
return false;
}
$saved = $_SESSION[$name];
unset($_SESSION[$name]);
return hash_equals($saved, $input);
}
}
上面的hash_equals函数用于定时字符串比较,可避免时序攻击。生成令牌后,我们在渲染表单的PHP页面中调用FormToken::generate(),把值放进隐藏输入框。校验时从$_POST取出对应字段传给check方法。
如果校验失败,说明是重复提交或非法请求,应当直接拒绝并返回提示,而不执行写入数据库等操作。成功后才继续业务逻辑。这样即使前端失效,后端依然铁面无私。
前端表单与令牌配合
原生表单写法非常简单,把令牌作为隐藏域输出即可。后端渲染时类似下面这段:
<form method="post" action="/submit.php">
<input type="hidden" name="form_token" value="<?php echo FormToken::generate(); ?>">
<input type="text" name="username">
<button type="submit" id="btn">提交</button>
</form>
为了更好体验,可以在提交按钮上做禁用处理,防止用户连点。若是ajax方式,则先用js读取隐藏域值,再用fetch发送,并在发送前禁用按钮,收到响应后再根据情况恢复。
document.getElementById('btn').addEventListener('click', function (e) {
e.preventDefault();
var btn = this;
var form = btn.closest('form');
var token = form.querySelector('input[name=form_token]').value;
btn.disabled = true;
fetch('/submit.php', {
method: 'post',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'form_token=' + encodeURIComponent(token) + '&username=' + encodeURIComponent(form.username.value)
})
.then(function (res) { return res.json(); })
.then(function (data) {
if (data.code !== 0) { btn.disabled = false; }
alert(data.msg);
})
.catch(function () { btn.disabled = false; });
});
这里要注意,令牌一旦被后端销毁,页面若未刷新就再次提交必然失败。所以ajax场景下,成功提交后应由后端返回新令牌,前端替换隐藏域,或干脆提交后跳转。否则用户想修改内容再提交就会碰壁。
另外多标签页问题:若两个标签页共用同一session,先提交的销毁令牌会导致另一页失效。可通过给令牌加前缀或每次生成不同name来缓解,但更常见的是接受此限制,依靠页面刷新解决。
完整后端接收示例
接收端submit.php可以这样写,先校验再处理,结构清晰:
<?php
session_start();
require 'FormToken.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = $_POST['form_token'] ?? '';
if (!FormToken::check($token)) {
header('Content-Type: application/json');
echo json_encode(['code' => 1, 'msg' => '重复提交或令牌失效']);
exit;
}
// 此处写数据库等安全逻辑
header('Content-Type: application/json');
echo json_encode(['code' => 0, 'msg' => '提交成功']);
}
这段代码把校验放在最前,任何未带有效令牌的请求都被挡掉。配合前端的按钮禁用与令牌刷新,就能在体验和安全间取得平衡。实践中还可把令牌有效期写入session,超过几分钟也判失效,进一步收紧防护。
常见误区与小结
有人以为前端disabled按钮就够了,但curl或脚本可绕开。也有人把令牌写死在html里不销毁,那等于没防。正确做法是服务端生成、单次消费、及时销毁,前端只做辅助。PHP的session机制已足够支撑中小站点,高并发时可换redis存储令牌,思路不变。
总体方案不复杂,却能有效堵住重复提交漏洞。把上述FormToken类纳入基础库,所有写操作表单统一接入,可大幅降低线上事故概率。
php表单重复提交csrf_token修改时间:2026-08-04 18:27:37