You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel框架下如何防止表单重复提交(后端解决方案)

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 03:59:11