@csrf不能防重复提交,因其token为会话级静态值,只验证请求来源合法性,不记录表单是否已处理;需用session()->pull()实现一次性令牌,并配合数据库唯一索引兜底。

CSRF 保护和防重复提交是两件事,@csrf 能拦跨站伪造请求,但拦不住用户手抖连点、页面卡顿后刷新重提——这两类问题必须分开处理,不能混为一谈。
为什么 @csrf 不能防重复提交
@csrf 的 token 是会话级静态值,在用户 session 有效期内始终不变。只要第一次提交还没返回,第二次请求仍能携带合法的 _token 进入后端,VerifyCsrfToken 中间件照常放行。它不记录“这个表单是否已被处理”,只验证“这是否来自本域合法会话”。
- 常见错误现象:
419 Page Expired是 CSRF 失败,而数据重复入库却没报错,说明 CSRF 生效了,但幂等性没做 - 别用
session()->get('token')+session()->forget()组合:并发场景下可能两次请求同时读到 token,然后都通过校验 - 必须用
session()->pull('key'),它是原子操作,读+删一步完成,避免竞态
用 session()->pull() 实现一次性表单令牌
这是 Laravel 5.5 原生支持、无需额外扩展的轻量方案,适合留言、注册、简单订单等非金融级场景。
- 在渲染表单的控制器里生成并存入 session:
session(['form_token_' . auth()->id() => Str::random(40)])(带用户 ID 避免多设备冲突) - Blade 表单中加隐藏字段:
<input type="hidden" name="form_token" value="{{ session('form_token_' . auth()->id()) }}"> - 提交入口处立即校验并清除:
if (! session()->pull('form_token_' . auth()->id(), false)) { return back()->withErrors(['message' => '请勿重复提交']); } - 注意:
pull()返回false表示 key 不存在或已取走,不要用=== null判定
数据库唯一索引才是最终防线
网络超时、F5 刷新、队列重试、Nginx 重发……这些情况会让任何内存/缓存层的防重失效。Laravel 5.5 的 DB::transaction() 配合唯一约束,才是生产环境兜底方案。
- 在迁移中添加业务唯一键,例如:
$table->unique(['user_id', 'email', 'created_at'])或更稳妥的order_sn字段 - 控制器中事务内捕获异常:
try { DB::transaction(function () use ($data) { Order::create($data); }); } catch (QueryException $e) { if ($e->getCode() === '23000' && strpos($e->getMessage(), 'Duplicate entry') !== false) { return response()->json(['message' => '订单已存在'], 409); } throw $e; } - MySQL 错误码
23000对应唯一约束冲突;PostgreSQL 是23505,需按实际数据库调整判断逻辑 - 别依赖前端生成的
idempotency_key:若用户禁用 JS 或篡改请求,该 key 就不可信;服务端应基于不可篡改字段(如auth()->id()+request()->ip()+md5(serialize($request->all())))生成并校验
Redis 分布式锁在 Laravel 5.5 中慎用
Laravel 5.5 默认未启用 Redis,且其 Cache::lock() 方法直到 7.x 才引入。强行用 Redis::set($key, $value, ['NX', 'EX' => 30]) 要自己处理连接、重试、释放逻辑,反而增加出错概率。
- 如果已配 Redis,可用
Cache::add('lock_' . $request->ip(), true, 10)做简易窗口限流(10 秒内同一 IP 只允一次) - 但注意:
Cache::add()不是原子锁,高并发下仍有极小概率失败,仅作辅助,不能替代数据库唯一约束 - 别在事务里嵌套 Redis 操作:若 DB 提交失败但 Redis key 已设,会导致后续正常请求被误拦
真正麻烦的不是写几行校验代码,而是得想清楚:这个表单在哪种异常路径下可能被重复触发?是用户行为、网络问题,还是上游服务重试?每种路径对应的技术手段不同,没有银弹——session token 挡不住 F5,唯一索引挡不住业务逻辑层的多次调用,Redis 锁挡不住 DB 层崩溃后的状态不一致。选方案前,先画清你的调用链和失败点。


















