防重复提交需服务端状态控制:用session()->pull()清除一次性Token,配合唯一索引和数据库事务兜底,Redis幂等需标准化key并设过期。

表单提交后按钮变灰没用?csrf_token() 不能防重复提交
很多人以为加了 @csrf 就万事大吉,其实它只防跨站请求伪造,对用户手抖连点、刷新页面重提、后退再提完全无效。真正要拦的是「同一份表单数据被多次入库」,核心得靠服务端状态控制。
实操建议:
- 前端禁用按钮只是体验优化,必须配合后端校验,否则绕过 JS 就失效
-
csrf_token()每次请求都一样(除非手动刷新 session),不能当唯一标识用 - 别依赖客户端时间戳或随机数——没签名、没服务端绑定,容易被伪造
用 session()->pull() 清除一次性 Token 最稳妥
Laravel 自带的 @csrf 是静态 token,但你可以自己生成一个「一次性的表单标识」,提交后立刻从 session 中取出并删除,第二次提交就找不到这个 key,直接拒绝。
常见错误现象:用了 session()->get() 但没删,导致重复提交仍能通过;或者用 session()->forget() 后没判空,报 undefined index。
使用示例(控制器中):
if (! session()->pull('form_token_' . $request->ip(), false)) {
return back()->withErrors(['message' => '请勿重复提交']);
}
要点:
- key 要带用户特征(如 IP 或
auth()->id()),避免多设备冲突 - 必须用
pull(),不是get()+forget()的组合,否则有竞态风险 - Token 应在表单渲染时写入 session:
session(['form_token_'.request()->ip() => Str::uuid()])
DB::transaction() + 唯一索引才是最终防线
即使做了前端限制和 session token,网络超时重试、队列任务重复执行等情况仍可能让同一条数据插入两次。这时候不能只靠逻辑判断,得靠数据库层兜底。
性能与兼容性影响:唯一索引几乎零性能损耗,比每次查库快得多;但要注意字段长度限制(比如 MySQL 对 TEXT 字段加唯一索引需指定前缀长度)。
实操建议:
- 对业务上「绝对不可重复」的组合字段建唯一索引,例如
(user_id, order_no)或(email, created_at) - 捕获
Illuminate\Database\QueryException,检查错误信息是否含SQLSTATE[23000](主键/唯一键冲突) - 别用
firstOrCreate()代替索引——它不是原子操作,在高并发下仍有概率重复插入
Redis 实现分布式幂等,但别小看 SETNX 的过期时间
集群部署或异步任务场景下,session 不共享,就得用 Redis。核心是 SETNX(set if not exists)+ 过期时间,但 Laravel 的 Cache::add() 默认不设过期,容易卡死。
容易踩的坑:
-
Cache::add('submit_'.$request->ip().'_'.$request->input('order_id'), true, 30)—— 看似合理,但如果请求卡在中间件,后续请求可能因缓存未命中而放行 - 没对 key 做标准化(比如没 trim 空格、没统一大小写),导致相同业务请求生成不同 key
- 忘记在成功后主动
Cache::forget(),虽然有过期,但冗余 key 积压影响运维
更稳的做法是:用 Cache::lock() 包裹关键逻辑,但注意它底层依赖 Redis 的 SET resource-name anystring NX PX 10000,必须确认 Redis 版本 ≥ 2.6.12。
复杂点在于:幂等 key 的设计要覆盖所有可能触发重复的维度(用户、动作类型、关键参数哈希),漏掉任意一个,就等于留了后门。











