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型別插入與修改資料時更新索引的效能消耗,你可能會放棄加這個索引。
總結
防止表單重複提交,功能雖然簡單,但要保證’萬無一失’,還是得費不少腦細胞,這是作為一個程式設計師該有嚴謹作風。