Laravel框架下如何防止表单重复提交(后端解决方案)
嘿,这个问题我做浏览器游戏开发的时候也踩过坑!完全懂你不想依赖客户端的想法——前端代码随便就能被篡改,后端兜底才是真安全。下面给你几个Laravel生态里实用的后端解决方案,从预防到兜底都有:
1. 一次性CSRF令牌(精准针对表单提交)
Laravel默认的CSRF令牌是会话级的,只要会话没过期就一直有效,这就给重复提交留了空子。我们可以改成一次性令牌,提交一次就失效:
步骤1:在视图里生成并存储一次性令牌
在表单所在的视图里,先生成一个随机令牌存在session里,再放到隐藏字段中:{{-- 生成并存储一次性令牌 --}} @php $constructToken = Illuminate\Support\Str::random(40); session()->put('construct_token', $constructToken); @endphp <form method="POST" action="{{ route('building.construct') }}"> @csrf <input type="hidden" name="construct_token" value="{{ $constructToken }}"> {{-- 其他表单字段 --}} <button type="submit">Construct</button> </form>步骤2:在控制器里验证并销毁令牌
处理请求时,先验证令牌是否匹配,匹配成功就立刻从session里删掉,再执行建造逻辑:public function construct(Request $request) { // 验证一次性令牌 $request->validate([ 'construct_token' => [ 'required', function ($attribute, $value, $fail) { if ($value !== session('construct_token')) { $fail('无效的请求,请重试。'); } }, ], // 其他字段验证规则 ]); // 销毁令牌,防止重复提交 session()->forget('construct_token'); // 执行建筑建造逻辑 // ... return redirect()->back()->with('success', '建筑开始建造!'); }这样就算用户长按回车,第二次请求时令牌已经失效,会直接被拦截。
2. 自定义防重复提交中间件(通用型方案)
如果你的游戏有很多类似的提交场景,可以写一个全局或路由级的中间件,用Redis/Session给请求加“锁”,短时间内同一用户的同一请求只能执行一次:
步骤1:创建中间件
php artisan make:middleware PreventDuplicateSubmissions步骤2:编写中间件逻辑
这里用Redis的setnx命令(不存在则设置)来实现锁,设置一个短过期时间(比如10秒,根据你的业务调整):<?php namespace App\Http\Middleware; use Closure; use Illuminate\Support\Facades\Redis; class PreventDuplicateSubmissions { public function handle($request, Closure $next) { // 只针对POST请求生效 if (!$request->isMethod('POST')) { return $next($request); } // 生成唯一请求标识:用户ID + 请求路径 + 请求体哈希(可选) $userId = auth()->id() ?? 'guest'; // 处理未登录用户(如果有的话) $requestKey = 'submit_lock:' . $userId . ':' . $request->path(); // 可选:加上请求体哈希,区分不同参数的同路径请求 // $requestKey .= ':' . md5(json_encode($request->all())); // 设置锁,过期时间10秒 $lockAcquired = Redis::setnx($requestKey, true); Redis::expire($requestKey, 10); if (!$lockAcquired) { return back()->withErrors('请求过于频繁,请稍后再试。'); // 如果是API请求,返回JSON: // return response()->json(['error' => '请求过于频繁'], 429); } return $next($request); } }步骤3:注册中间件
在app/Http/Kernel.php里把中间件加到路由中间件组里(比如web组),或者单独给需要的路由使用:protected $routeMiddleware = [ // ... 'prevent.duplicate' => \App\Http\Middleware\PreventDuplicateSubmissions::class, ];然后在路由里使用:
Route::post('/building/construct', [BuildingController::class, 'construct'])->middleware('prevent.duplicate');
3. 数据库唯一约束(最后一道兜底防线)
前面的方案都是预防,但万一出现极端情况(比如Redis宕机、令牌验证漏过),数据库层面的约束能彻底阻止重复记录。
假设你的建筑表是buildings,每个用户同一时间只能建造一个同类型的建筑,可以给user_id和building_type加联合唯一索引:
步骤1:创建迁移(或修改现有迁移)
php artisan make:migration add_unique_constraint_to_buildings_table迁移文件内容:
public function up() { Schema::table('buildings', function (Blueprint $table) { $table->unique(['user_id', 'building_type']); }); } public function down() { Schema::table('buildings', function (Blueprint $table) { $table->dropUnique(['user_id', 'building_type']); }); }执行迁移:
php artisan migrate步骤2:控制器里捕获约束冲突异常
当用户重复提交时,数据库会抛出唯一约束冲突的异常,我们捕获并返回友好提示:use Illuminate\Database\QueryException; public function construct(Request $request) { try { // 执行建筑建造逻辑 auth()->user()->buildings()->create([ 'building_type' => $request->building_type, // 其他字段 ]); return redirect()->back()->with('success', '建筑开始建造!'); } catch (QueryException $e) { // 唯一约束冲突的错误码是23000 if ($e->getCode() === '23000') { return redirect()->back()->withErrors('你已经在建造这个类型的建筑了!'); } // 其他异常正常抛出 throw $e; } }
4. 辅助方案:Laravel内置的Throttle中间件
Laravel自带的throttle中间件可以限制短时间内的请求次数,虽然不是专门针对重复提交,但可以作为辅助手段,比如限制用户1分钟内最多提交5次请求:
Route::post('/building/construct', [BuildingController::class, 'construct'])->middleware('throttle:5,1');
方案选择建议
- 如果是单一表单场景,用一次性CSRF令牌最精准;
- 如果有多个提交场景,用自定义中间件更高效;
- 数据库唯一约束一定要加,作为最后兜底;
throttle中间件可以搭配使用,防止恶意刷请求。
内容的提问来源于stack exchange,提问作者viko91

