HTML表单无原生防重能力,唯一可靠方案是JavaScript监听form submit事件动态禁用所有提交按钮,并配合后端生成的一次性submit_token隐藏字段校验。

HTML 表单元素本身没有防重复提交能力,disabled、novalidate、autocomplete 等属性都不能拦截重复请求——真正起作用的只有 JavaScript 监听 submit 事件 + 后端一次性 token 校验。
监听 form 的 submit 事件禁用所有提交按钮
这是前端最基础也最不可跳过的动作。只禁用一个按钮、只监听 click、或写在 onclick 里,都会漏掉回车提交、多个提交按钮、JS 主动调用 form.submit() 等路径。
- 必须用
form.addEventListener('submit', ...),不是button.addEventListener('click', ...) - 要选中全部提交控件:
form.querySelectorAll('button[type="submit"], input[type="submit"]'),不能只找#submit-btn - 禁用后必须在
fetch或XMLHttpRequest的finally块中恢复,否则失败时按钮永远灰掉 - 别用
pointer-events: none替代disabled:它不阻止键盘聚焦、不被屏幕阅读器识别、也不阻断 JS 调用
用 hidden input 带 submit_token,且必须由后端生成
<input type="hidden" name="submit_token" value="..."> 这行 HTML 本身不防重,但它让 token 能随表单自动发出——前提是 value 是服务端每次渲染页面时动态签发的。
- value 不能是前端
Math.random()或crypto.randomUUID()生成的(可被脚本绕过) - token 必须绑定当前用户 session + 表单类型(如
login_form),不能全局复用 - 有效期建议 5–15 分钟;过期或已消费的 token,后端必须返回
400 Bad Request并带明确错误信息 - 如果改用
fetch提交,得手动取值:document.querySelector('[name="submit_token"]').value
为什么不能只靠前端禁用按钮
禁用按钮只是交互层防护,对真实风险几乎无效。用户刷新页面、新开标签页、用 Postman 发请求、甚至禁用 JS,都能绕过它。
立即学习“前端免费学习笔记(深入)”;
- 网络超时后按钮已禁用,但请求根本没发出去,用户误以为“已提交”
- 用户按 F5 刷新结果页,若后端没走 PRG 模式,会再次 POST 原始表单数据
- 多个标签页打开同一表单页,各自生成不同 token,但后端若没校验 session 绑定,仍可能接受重复
- 数据库唯一约束(如
UNIQUE(username))是兜底手段,但报错体验差,且无法防止非 DB 层副作用(如发两次短信、扣两次款)
后端必须做幂等校验,且不能只校验时间戳
前端传来的任何字段(timestamp、req_id、nonce)若未经服务端签名和存储验证,都等于没防。
- 推荐组合:
X-Idempotency-Key请求头 + Redis 记录已处理 key(含过期时间) - 避免用纯客户端生成的
req_id做唯一判断——攻击者可提前构造并重放 - PRG 模式(POST → 302 Redirect → GET)必须配合使用,否则 F5 和前进/后退仍会触发重复 POST
- 哪怕用了 token,后端也要在业务逻辑前加一层“是否已处理该 token”的原子操作,不能依赖应用层 if 判断
最容易被忽略的一点:token 校验通过后,必须立即标记为“已消费”,且这个操作要和业务写入在同一事务里——否则高并发下仍可能被两个请求同时读到未消费状态,造成漏防。



















