可靠防重复提交需前后端协同:前端监听form submit事件统一拦截并禁用控件,后端生成一次性submit_token绑定session与表单类型、持久化校验状态,并配合幂等性设计。

单纯靠 disabled 属性或内联 onclick 禁用按钮,无法真正防止重复提交——它既拦不住回车触发、也防不了 JS 主动调用 form.submit(),更扛不住刷新后重发。可靠方案必须同时满足:前端锁住提交动作入口 + 后端验证幂等性。
监听 submit 事件统一拦截
所有提交路径(点击按钮、按回车、JS 调用)最终都会触发 form 元素的 submit 事件,这是唯一可信赖的拦截点。
- 必须用
event.preventDefault()阻止默认行为,否则禁用按钮无效 - 禁用范围要覆盖全部提交控件:
button[type="submit"]和input[type="submit"],不能只锁一个 - 禁用后立即调用
form.submit()(原生同步提交),不要在中间插入await或fetch,否则 submit 事件可能被绕过 - 避免用
form.onsubmit = ...赋值方式,它会覆盖其他监听器;优先用addEventListener
submit_token 必须由后端生成并一次性消费
前端传的 submit_token 字段本身不防重,它的价值在于让后端能识别“这是第几次合法请求”。
- token 值不能是前端
Math.random()或时间戳拼接,必须由后端生成(如 UUID + HMAC 签名) - 必须绑定当前用户 session 和表单类型(例如
order_form),不能全局复用 - 有效期建议 5–15 分钟:太短导致弱网用户失败,太长增加重放风险
- 服务端校验通过后必须立即将该 token 标记为“已消费”,且状态需持久化(Redis 或 DB),不能仅存在内存里
- 校验失败应返回明确 HTTP 状态(如
400 Bad Request)和错误信息,前端据此提示刷新页面
用 AbortController 配合 pending 状态防 fetch 并发
当表单改用 fetch 提交而非原生 form.submit() 时,禁用按钮只能防点击,拦不住快速连点触发多个 fetch 请求。
- 声明一个
let pending = false变量,在发起请求前判断:若为true则直接return - 每次新请求都新建
AbortController,并在发起前调用上一个实例的abort() -
finally块中必须重置pending = false并恢复按钮状态,否则失败后用户无法重试 - 注意:
AbortController不会取消已响应的请求,只中断未完成的网络传输 - 不要把 abort 逻辑写在
catch里——它应在请求发起前就生效
服务端幂等校验不能省略
前端任何控制都是“尽力而为”。用户开开发者工具、用 Postman、刷新页面、F5 重发,都可能绕过所有前端防护。
- 推荐使用
X-Idempotency-Key请求头,值由前端生成(如crypto.randomUUID()),服务端查重并缓存结果 - 数据库唯一索引只是兜底,不能作为主要防重手段——因为超时未收到响应时,用户会重试,而此时 DB 可能已写入但前端不知情
- 状态机设计要区分“处理中”“已完成”“已失败”,避免因重试导致业务逻辑错乱
- POST 后跳转推荐用
303 See Other,由浏览器自动重定向,天然规避 F5 重复提交
最常被忽略的是:token 过期时间与用户操作节奏不匹配,以及服务端未将“处理中”状态持久化。这两个点一旦出问题,前端再严密的锁按钮也没用。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











