按钮点击即禁用,需在click回调首行设disabled=true并启动倒计时,而非等待接口返回;服务端须用Redis按手机号+时间窗口限频,前端倒计时仅为体验层防护。

按钮点击即禁用,不是等接口返回才锁
用户点一下就疯狂连点,本质是前端没在第一时间切断触发通路。正确做法是:click 回调一进来就设 button.disabled = true,同时启动倒计时,而不是等 fetch 或 axios 的 .then 才去锁。网络慢、接口卡住时,重复请求照样发出去——这是最常踩的坑。
常见错误现象:console.log 显示发了 3 次请求,但后端只收到 1 次成功响应;或者短信平台后台看到同一手机号 10 秒内被调了 7 次 /send-sms 接口。
- 倒计时从 60 开始递减,期间按钮保持
disabled状态 - 倒计时结束前,即使用户手动删掉
disabled属性,JS 也要重新加回去(靠监听或定时校验) - 别依赖 CSS 类名控制禁用状态,比如只加
class="disabled"而不设disabled属性——键盘Enter仍能触发表单提交
防刷不能只靠前端倒计时
前端倒计时只是体验层防护,对 curl、Postman 或自动化脚本完全无效。真正起效的限制必须落在服务端:按手机号 + 时间窗口(如 60 秒)做 Redis 计数器,超限直接 429 返回。否则,攻击者绕过页面,写个循环脚本就能刷爆你的短信额度。
使用场景举例:用户填了 138****1234,点击发送,后端收到请求后立即查 redis.get("sms:138****1234:20260716"),值 ≥ 5 就拒绝,且不生成验证码。
立即学习“前端免费学习笔记(深入)”;
- IP 限频只能作辅助,动态 IP 或代理池很容易绕过
- 必须绑定手机号维度,因为一个 IP 下可能有多个合法用户
- Redis key 建议带日期后缀(如
sms:{phone}:{yyyymmdd}),避免跨天计数不清 - 前端倒计时时间(如 60s)和服务端窗口时间必须严格一致,否则用户会遇到“明明倒计时完了,点还是提示‘请稍后再试’”
按钮状态要和真实请求生命周期解耦
很多人把按钮解锁逻辑写在请求的 .finally 里,结果接口超时或网络中断时,按钮永远卡在禁用态。更稳妥的做法是:倒计时到 0 自动解锁,不管请求是否完成;同时用独立变量标记“当前是否有未决请求”,避免倒计时还没完用户又点了别的操作路径。
参数差异示例:倒计时用 setTimeout 控制 UI,而请求状态用布尔变量 isSending 单独管理,两者不耦合。
- 不要在
fetch().catch()里重置倒计时,失败也要等满 60 秒才能再发 - 如果用户中途关闭页面,服务端依然要完成限频计数,否则下次打开还能立刻再发
- 移动端需额外处理:Safari 某些版本下,
button.disabled在表单提交时可能失效,建议配合event.preventDefault()双保险
隐藏字段 + 时间戳 + 服务端校验才是完整链路
单纯按钮禁用 + 倒计时 + 后端限频,仍可能被批量脚本攻破。必须叠加基础反自动化手段:在表单里塞一个 input type="text" class="hidden-field" name="honeypot",CSS 设为 display:none;同时埋一个 <input type="hidden" name="ts" value="1721137890123">。后端收到后,检查 honeypot 是否为空、ts 是否在当前时间 ± 5 分钟内。
容易忽略的细节:这个 ts 必须是服务端生成并注入 HTML 的,不能用 Date.now() 前端算——否则攻击者可伪造任意时间戳。
-
honeypot字段名别叫honeypot,换成类似user_agent_hint这种看起来像正常字段的名字 - 所有校验失败统一返回
{"code":400,"msg":"请求异常"},不暴露具体是时间戳错还是蜜罐被填 - 验证码图片的
src必须带唯一参数(如/captcha?t=1721137890123&v=abc123),防止浏览器缓存复用旧码



















