1. 程式人生 > >laravel中防止表單重複提交的綜合解決方案

laravel中防止表單重複提交的綜合解決方案

怎樣防止表單重複提交,通過搜尋引擎能搜到很多結果,但很零散,系統性不強,正好前幾天做了這個功能,決定記錄下來。
根據資料流向的過程,分別在三個“點”控制表單的重複提交,如下:

第一,使用者觸發submit時,前端js控制提交按鈕的狀態,使用者觸發提交即設定按鈕的disabled屬性為true,防止重複點選;
第二,在資料到達伺服器並通過驗證時,服務端根據維護的一個狀態以控制表單重複提交,通常是利用session;
第三,資料入庫時,資料庫新增unique索引,當然這個得根據需要來選擇;

下面詳細說各部分的內容:

1、前端js控制按鈕狀態

監聽表單的submit事件,在其中將提交按鈕設定為不可用,程式碼如下:

$('form').submit(function() {
        $('button[type=submit]').attr('disabled', true);
    });

基本上禁用按鈕後,前端不用管了,因為資料如果通不過laravel的驗證或發生異常,在laravel中的做法是back()->withInput()->withErrors();這時頁面會重新整理,按鈕會自行恢復狀態。

如果是ajax方式提交,直接在function裡傳送ajax請求前禁用就行了,然後根據請求的結果來恢復按鈕的狀態或跳轉頁面就可以了;

2、服務端通過維護一個狀態來控制表單重複提交

網路上常用做法解析

網上搜了一下,說得最多的做法是:

在顯示錶單頁面時,服務端生成一個隨機字串並以該字串為key儲存在session中並將其回顯在表單一個隱藏的input中,當提交表單時,服務端根據這個隱藏input的值(即session中的key)去session中取值,如果該key存在於session中表示正常提交,並立即從session中刪除該key,若發生重複提交,session中的這個key已經被刪除了,就可以給前端相應的提示“表單重複提交”。

這種做法有兩個弊端

a、重新整理介面,會導session中存放了多個key,資料冗餘且存在漏洞,因為存在多個key即意味著同一時間可以使用不同key來提交同一份資料;

【補充】laravel中可以通過flash方法來儲存只在下個請求有效的session資料,即在下一請求之後,該資料會被自動從session中清除,這樣確實能解決重新整理介面後session中儲存多個key的問題,但會帶來一個新的問題,列舉一個場景加以說明:假如某使用者正在寫評論,寫到一半被旁邊推薦的一篇文章吸引,就先去看文章了,等看完回來繼續寫完評論提交,會發生什麼事?會被當做表單重複提交處理,因為檢視文章時,已經將flash方式儲存的session清空了。

b、不夠簡潔,要知道這裡解決的問題是要防止表單重複提交,完全沒有必要生成一個動態的類似token東西,針對某一類表單提交(如註冊)將儲存在session中的key固定就好了,這樣就可以省去form中那個隱藏的input了。

推薦做法

總體思路

針對不同型別的表單(這裡定義登陸、註冊為不同型別的表單)服務端維護多個不同的key(比如登陸表單在session中對應的key固定為‘login’,登錄檔單的key固定為’register’),在顯示錶單頁面時將key儲存進session(對應的value可以存1,也可以存當前時間,存當前時間的話,你可以根據在提交表彰時根據時間間隔來作進一步的控制),表單提交時將其刪除,若出現重複提交,session中不存在這個key,你就可以提示使用者“不要重複提交”了。

分步實現(以註冊為例)

1、在controller中顯示註冊介面的方法裡儲存session

public function showRegistrationForm(Request $request)
{       
     $request->session()->put('register',time());
     return view('auth.register');
}

2、在處理表單提交方法中判斷是否重複提交

public function register(Request $request)
{
    if($this->request->session()->has(‘register’)){
        //存在則表示是首次提交,清空session中的'register'
        $this->request->session()->forget(‘register’);
    }else{
        //否則拋http異常,跳轉到403頁面
        throw  new HttpException(403,'請忽重複註冊');
    }

    //省略下面的驗證、註冊邏輯等程式碼
}

【補充】如果是引數驗證失敗,比如手機號已註冊之類的,你back()->withInput()->withErrors();是會重新執行showRegistrationForm()方法的,所以出錯後再次提交是不會被當做重複提交處理的

簡單封裝程式碼

<?php
namespace App\Http\Controllers;

use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\Request;

/**
 * 基礎控制器,封裝了web及api請求的一些公共方法
 * @author 94505
 *
 */
class Controller extends BaseController
{
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;

    /**
     * 請求
     *
     * @var Request
     */
    protected $request;

    public function __construct()
    {
        $this->request = app('request');
    }
    /**
     * 防止表單重複提交的key字首
     * @var string
     */
    private $formResubmitPrefix = 'f_';

    /**
     * 將key加個字首
     * @param unknown $key
     * @return string
     */
    private function formResubmitKeyProcess($key){
        if(empty($key)){
            //預設使用當前路由的uri為key
            return $this->formResubmitPrefix.Route::current()->uri;
        }else{
            return $this->formResubmitPrefix.$key;
        }
    }
    /**
     * 在初始化表單前呼叫(如上面分步實現中的showRegistrationForm()方法中)
     * @param unknown $key
     */
    protected function formInit($key = null){
        $key = $this->formResubmitKeyProcess($key);
        $this->request->session()->put($key,time());
    }
    /**
     * 在處理表單提交的方法中呼叫(如上面分步實現中的register()方法)
     * @param string $message
     * @param unknown $key
     * @throws HttpException
     */
    protected function formSubmited(string $message = '請忽重複提交!',$key = null){
        $key = $this->formResubmitKeyProcess($key);
        if($this->request->session()->has($key)){
            $this->request->session()->forget($key);
        }else{
           throw  new HttpException(403,$message);
        }
    }        
    /**
     * 在處理表單提交的方法中呼叫(如上面分步實現中的register()方 法),該方法方便自定義重複提交時的提示頁面,可以在子類中if判斷一下,如果發生重複提交,響應自定義的介面
     * @param string $message
     * @param unknown $key
     */
    protected function formSubmitIsRepetition(string $message = '請勿重複提交!',$key = null){
        $key = $this->formResubmitKeyProcess($key);
        if($this->request->session()->has($key)){
            $this->request->session()->forget($key);
            return false;
        }else{
            return response()->view('errors.403',['message'=>$message],403);
        }
    }
   /**
     * 該方法用於ajax請求,返回的資料是陣列
     * @param string $message
     * @param unknown $key
     */
    protected function formSubmitedForAjax(string $message = '請勿重複提交!',$key = null){
        $key = $this->formResubmitKeyProcess($key);
        if($this->request->session()->has($key)){
            $this->request->session()->forget($key);
            return false;
        }else{
            return ['result'=>'fail','message'=>$message];
        }
    }
}

在需要防止表單重複提交的控制器內,繼承上面封裝的Controller就可以直接呼叫裡面的方法了,記得在子類構造方法中呼叫parent::__construct();,不然$this->request會為null,當然你也可以改成用全域性Session輔助函式session()。

3、資料庫控制,新增unique索引

資料庫加unique索引就不詳細說了,只能根據實際情況權衡決定,比如使用者表的手機號(列phone)可用來登陸,必須要求唯一,但在大多數情況下你無法加這個索引,因為現在一般都支援多種登陸方式,如微信登陸、微博登陸,這個手機號可能會沒有值,除非程式自動生成一個,但是否有必要?再比如一個varchar型別的列,雖然資料是唯一的,也不會出現空的情況,考慮到varchar型別插入與修改資料時更新索引的效能消耗,你可能會放棄加這個索引。

總結

防止表單重複提交,功能雖然簡單,但要保證’萬無一失’,還是得費不少腦細胞,這是作為一個程式設計師該有嚴謹作風。