PHP防止表单重复提交

24 篇文章 1 订阅

日常开发中,表单的提交是无法避免的,而我们必须熟知的一点是“在做后退或刷新操作时,post会重新提交请求是有害的,而get虽然会重新获取数据但却是无害的”。所以我们要禁止用户重复提交表单。

首先,我们要知道在什么情况下表单会重复提交

  1. 点击提交按钮两次。
  2. 点击刷新按钮。
  3. 回退,然后重复操作。
  4. 进行一些恶意操作。

那么,如何防止表单重复提交呢?

其实很简单,根据数据流向的过程,可以从三个层面进行控制:

1、前端层面

用户点击按钮触发submit时,前端js控制提交按钮的状态,将按钮的disabled属性为true,防止重复点击。

//监听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已经被删除了,就可以给前端相应的提示“表单重复提交”。

缺点:

  1. 刷新界面,会导session中存放了多个key,数据冗余且存在漏洞,因为存在多个key即意味着同一时间可以使用不同key来提交同一份数据;

    【补充】laravel中可以通过flash方法来存储只在下个请求有效的session数据,即在下一请求之后,该数据会被自动从session中清除,这样确实能解决刷新界面后session中保存多个key的问题,但会带来一个新的问题,列举一个场景加以说明:假如某用户正在写评论,写到一半被旁边推荐的一篇文章吸引,就先去看文章了,等看完回来继续写完评论提交,会发生什么事?会被当做表单重复提交处理,因为查看文章时,已经将flash方式保存的session清空了。

  2. 不够简洁,要知道这里解决的问题是要防止表单重复提交,完全没有必要生成一个动态的类似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索引的话只能根据实际情况权衡决定。

比如用户表的手机号(列phone)可用来登陆,必须要求唯一,但在大多数情况下你无法加这个索引,因为现在一般都支持多种登陆方式,如微信登陆、微博登陆,这个手机号可能会没有值,除非程序自动生成一个,但是否有必要?再比如一个varchar类型的列,虽然数据是唯一的,也不会出现空的情况,考虑到varchar类型插入与修改数据时更新索引的性能消耗,可能会放弃加这个索引。

 

作为一个严谨的程序员,防止表单重复提交处理是必须的!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值