表单防重必须服务端校验,前端禁用按钮仅防手抖;hidden token需服务端动态生成、带签名时效、校验后立即销毁,并配合数据库唯一索引与PRG模式。

表单提交时禁用按钮只是障眼法,不是防重本身
禁用 button[type="submit"] 或 input[type="submit"] 能拦住用户手抖连点,但拦不住刷新页面后重提、多标签页并发、或 curl/Postman 直接发请求。它只解决“看得见的重复”,不解决“逻辑上的重复”。
常见错误是:只写 btn.disabled = true 就以为万事大吉;更糟的是在 fetch 的 .finally() 里无条件恢复按钮——失败时不该放行,成功后也不该让用户误以为还能再提一次。
- 必须配合服务端校验,否则前端禁用毫无意义
- 禁用前建议先调用
form.checkValidity(),避免校验失败还锁死按钮 - 回车触发表单提交也会走
submit事件,所以监听form.addEventListener('submit', ...)比监听按钮click更可靠
hidden 字段 value 必须每次动态生成,不能硬编码
input type="hidden" 本身不防重,它只是个运输容器。如果 value 写死(比如 value="abc123")或前端用 Math.random() 生成,整个机制就失效了。
正确做法是服务端在渲染页面时生成 token,并存入当前 session:
立即学习“前端免费学习笔记(深入)”;
session_start();
if (!isset($_SESSION['submit_token'])) {
$_SESSION['submit_token'] = bin2hex(random_bytes(32));
}
$token = $_SESSION['submit_token'];
然后塞进 HTML:<input type="hidden" name="submit_token" value="<?php echo htmlspecialchars($token); ?>">
- 每次页面加载都必须刷新 token,旧 token 失效
- 绝对不能缓存含该 hidden 字段的 HTML 页面
- 输出前必须过
htmlspecialchars(),否则可能 XSS
后端校验必须在业务执行前完成,且立刻销毁 token
收到 POST 后,第一件事不是处理数据,而是比对 $_POST['submit_token'] 和 $_SESSION['submit_token'] 是否一致。不一致就直接返回 400 Bad Request,不走后续逻辑。
关键动作是校验通过后立刻 unset($_SESSION['submit_token']),而不是等响应结束再清理——否则并发请求可能同时命中同一个 token。
- 前后端字段名必须完全一致:
name="submit_token"对应$_POST['submit_token'],大小写敏感 - 校验失败不能只 echo 提示,要明确 HTTP 状态码,前端才能判断是否需刷新页面
- token 不带签名、不设有效期(如 10 分钟)、不绑定 session ID,等于没做
绕过表单的重复请求,hidden 字段完全无效
攻击者根本不用打开你的 HTML 页面。他可以直接构造 POST 请求,伪造任意 submit_token 值。所以 token 必须带服务端可验证的签名(比如 HMAC-SHA256),且包含时间戳和 session ID。
仅靠 hidden 字段 + session 校验,依然挡不住重放攻击。必须叠加:
- 数据库唯一索引(如订单号唯一)作为兜底
- PRG 模式(POST → 302 Redirect → GET),避免 F5 触发二次 POST
- 幂等 key(如
Idempotency-Keyheader)用于 API 场景
最容易被忽略的是:hidden 字段一旦嵌入,就会让人误以为“已防重”,而真正脆弱的其实是服务端 token 的生成与消费逻辑——它看起来静态,实则必须动态、有时效、有签名、有状态标记。



















