真正解决表单重复提交问题php代码
真正解决表单重复提交问题php代码
以前用的js表单防止重复提交方法代码如下 | |
<script type="text/javascript"> var checkSubmitFlg = false; function checkSubmit() { if (!checkSubmitFlg) { // 第一次提交 checkSubmitFlg = true; return true; } else { //重复提交 alert("Submit again!"); return false; } } </script> //以下三种方式分别调用 <form onsubmit="return checkSubmit();"> <input type="submit" onclick="return checkSubmit();" /> <input type="button" onclick="document.forms[0].action='./test';document.forms[0].submit();return checkSubmit();" /> |
这样如果我直接做一个表单,然后提交给/test,上面代理就是一个摆设了,那我们要如何解决此问题
如果您已经知道如何解决的话那么这篇文章可能不适合你的口味,paperen这里也打算从基础开始讨论,所以希望一步看到解决方案的您也可能不太适合,所以请注意。So~开始吧 ~
paperen想您一定知道表单是什么吧,form元素就是表单,一般网页需要输入的地方必定使用了表单元素,也很常见,一般的代码如下:
代码如下 | |
<form method="post"> <p> <label for="test">随便输入点什么www.45it.com</label> <input type="text" name="data" /> </p> <p> <input type="submit" value="提交" name="submit" /> </p> </ul> </form> |
重点其实是form与input元素,p元素只是paperen私自加上去的,对后续的说明没有任何影响,其实很简单,所谓input就是输入了,你可以完全将input 元素理解为是用作用户输入,只是某些属性的(type)不能作为输入而已(这里就是submit),而form元素你完全可以将它理解为是一个袋子,将所有用户输入数据到装在它里面之后用 来提交回服务端处理,但对于form元素值得注意的是method属性,一般来说有get与post两种方法,其实不要想得太复杂(因为深入的不需要太理解,对于后续的内容没有太多关系,如 有兴趣不妨可以使用浏览器的调试工具查看请求头部信息与发送信息,例如firebug),表现出来就是,使用get提交表单的话所有的input元素的值将会在地址栏处出现,而post则不会, 例如使用get提交此表单后的浏览器地址栏
代码如下 | |
http://localhost/mytest/token/form.php?data=test&submit=%E6%8F%90%E4%BA%A4 |
post则在 地址栏看不到了,使用fiebug可以看到如下信息
可以简单认为get是显式传送数据的,而 post则是隐式传送数据的,但还有一个很大区别的是post支持更多更大的数据传送。
Next,当表单代码写好了,那么让我们来进行服务器脚本的编写(这里就是PHP)。很简单 ~
代码如下 | |
<?php if ( isset( $_POST['submit'] ) ) { //表单提交处理 $data = isset( $_POST['data'] ) ? htmlspecialchars( $_POST['data'] ) : ''; //Insert or Update数据库 $sql = "insert into test (`string`) values ('$data')"; //do query echo $sql; } ?> |
因为这里是post传送数据的,所以使用PHP的$_POST全局变量就能获取到表单提交的数据,所有使用post方法的表单数据提交到服务端都会被保存在这个$_POST全局变 量中,不妨可以试试print_r( $_POST )这个变量你就明白了。
首先检查一下是否在$_POST数组里面存在submit,如果存在则证明是表单提交过来的,正如asp.net中好像有个 叫ispostback的一样,只是这样没那么严谨而已,但是不要紧之后会解决这个问题的。
之后接收输入框的数据,就是$_POST['data'],别忘了使用htmlspecialchars对这个进 行一下html过滤,因为防止输入了html标签或javascript造成问题(貌似叫做XSS漏洞)。最后就是拼接到sql语句中送入数据库跑了(只是这里paperen并没有很详细使用一些操作数据库的 函数例如mysql_query,有兴趣自己完成它)。恭喜,到了这里你已经顺利地完成了一个数据录入的功能了,但是有个地方你总得改善吧,插入数据后总得给操作者一个提示吧~~至少提示 我操作失败还是成功。所以整个代码paperen写成以下样子。
代码如下 | |
<?php if ( isset( $_POST['submit'] ) ) { //表单提交处理 $data = isset( $_POST['data'] ) ? htmlspecialchars( $_POST['data'] ) : ''; //connect mysql_connect( 'localhost', 'root', 'root' ); //select db mysql_select_db( 'test' ); //设置字符集防止乱码 mysql_query( 'set names "utf8"' ); //SQL $sql = "insert into `token` (string) values ('$data')"; //query mysql_query( $sql ); $insert_id = mysql_insert_id(); if ( $insert_id ) { $state = 1; } else { $state = 0; } } ?> <?php if ( isset( $state ) && $state ) { //数据插入成功 ?> <p>插入成功 <a href="form.php">返回</a></p> <?php } else { //失败或者没有插入动 作 ?> <form method="post"> <p> <label for="test">随便输入点什么</label> <input type="text" name="data" /> </p> <p> <input type="submit" value="提交" name="submit" /> </p> </ul> </form> <?php } ?> |
html的声明与head还有body都省略了,对比于一开始的代码其实主要是实现了真正插入数据库动作与给出 了操作反馈(通过$state变量),不妨自己拷贝代码然后试试(当然请根据自己实际情况修改数据库操作部分的代码)。代码正常,逻辑没问题,但是有个问题,就是在显示插入成功后再刷新页 面又会执行了表单处理动作,又插了一遍数据!这就是所谓的重复插入问题。在放出解决方案之前您可以自己思考一下该如何解决。
你会不会认为是接收数据与显示处理结果都是 这个页面所以才会导致这个问题?也对,也可以这么认为,使用一些调试工具你会发现,浏览器还对post的数据进行了保留,故在提交完表单后再刷新的话该post数据会重新提交了一遍。
如果有办法将浏览器的这个临时保存的post数据清空掉不就解决问题了,但服务端是没法 做到这点的,因为这是浏览器自身的事情,要么我们就重定向了不然再刷新还是会重复提交数据。
到目前为止也许你已经了解到重复提交的意思与问题的恶劣所在,如果 你不是选用重定向的办法那么就得另外想一个办法了,所以令牌解决办法就是这么过来的。
正如令牌本身代表着权限,操作权,身份标志等等,所以我能不能为我的表单加上这么 一个身份标志,在客户端每次请求这个表单的时候同时生成一个令牌其挂钩,在提交时再进行判断,正确则接收并处理表单。实现本质就是如此,而反映到具体实现上,就需要用到一种叫 session的东西。关于session的解析,参见wiki
简单的理解就是session也是一种令 牌的概念,所以你可能会很惊奇,“什么我已经使用了令牌?!”,是的,但是我们要实现的不仅仅是session而是在其基础上附加一些数据来实现我们想要的表单令牌。So let's do it!
session在php中也是被存放在$_SESSION这个超级全局变量里面的,启用起请使用session_start(),关于其他服务端脚本原理一样,只是可能调用方法名不一致而已。加入 token后的代码如下:
代码如下 | |
<?php //开启session session_start(); if ( isset( $_POST['submit'] ) && valid_token() ) { //表单 提交处理 } /** * 生成令牌 * @return string MD5加密后的时间戳 */ function create_token() { //当前时间戳 $timestamp = time(); $_SESSION['token'] = $timestamp; return md5( $timestamp ); } /** * 是否有效令牌 * @return bool */ function valid_token() { if ( isset( $_SESSION['token'] ) && isset( $_POST['token'] ) && $_POST['token'] == md5( $_SESSION['token'] ) ) { //若正确将本次令牌销毁掉 $_SESSION['token'] = ''; return true; } return false; } ?> <?php if ( isset( $state ) && $state ) { //数据插入成功 ?> <p>插入成功 <a href="form.php">返回 </a></p> <?php } else { //失败或者没有插入动作 ?> <form method="post"> <p> <label for="test">随便 输入点什么</label> <input type="text" name="data" /> </p> <p> <input type="submit" value="提 交" name="submit" /> </p> </ul> <!-- Token --> <input type="hidden" value="<?php echo create_token(); //生成 令牌 ?>" name="token" /> <!-- Token --> </form> <?php } ?> |
部分代码paperen这里省略,因为并不是重点,其实加的 东西只有3处:
第一,在表单结束前加入了一个input元素,记得type为hidden(隐藏域)
第二,增加了两个函数,create_token与valid_token,前者用来生成令牌 的后者用来验证令牌的
第三,在if中多加一个条件,valid_token
那就大功告成了,很简单,而且所有的东西都聚集在新加的两个函数中。paperen这里使用的令牌很 简单就是时间戳,将请求表单时的时间戳存储到$_SESSION['token']中,那么验证令牌就明白了,就是检查客户端提交过来的$_POST['token']是否与md5后的$_SESSION['token']一致 就行了,当然还要加上存在$_POST['token']与$_SESSION['token']这两个变量才行。
你可以将这个简单的令牌模式封装得更加棒并扩展一下功能,例如加上表单提交超时验证 也是个不错的动手机会。
最后附上之前paperen扩展codeingeter的Form_validation类文件,主要是扩展上令牌与表单超时。压缩包中welcome.php是控制器文件,请放置到 applicationcontroller中(如不想增加这个控制器可以打开然后将token方法复制下来放到已有的其他控制器中);MY_Form_validation.php请放到applicationlibraries中。
codeingeter的Form_validation类文件代码
代码如下 | |
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); class Welcome extends CI_Controller { public function index() { $this->load->view('welcome_message'); } public function token() { $this->load->helper( array('form') ); $this->load->library('form_validation'); if ( $this->input->post( 'submit' ) && $this->form_validation->valid_token() ) { //nothing //valid_token已经包含令牌超时与令牌正确的判断,若要启用令牌超时,请将token_validtime设置为非0 echo 'ok'; } //生成表单令牌 $token = $this->form_validation->create_token(); //form example echo form_open(); echo form_input('token', ''); echo $token; echo form_submit('submit', 'submit'); echo form_close(); } } |
form_validation_token
代码如下 | |
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); /** * @abstract 继承CI的Form_validation类在其基础上增加令牌 */ class MY_Form_validation extends CI_Form_validation { /** * 令牌键值 * @var string */ var $key = 'token'; /** * 表单令牌有效时间(秒) * @abstract 如果某些表单需要限制输入时间,则设置该值,为0则不限制 * @var int 秒 */ var $token_validtime = 5; /** * 调试模式 * @var bool */ var $debug = false; /** * CI对象 * @var <type> */ private $_CI; public function __construct() { parent::__construct(); $this->_CI =& get_instance(); //如果配置没有填写encryption_key $encryption_key = config_item('encryption_key'); if ( empty( $encryption_key ) ) $this->_CI->config->set_item( 'encryption_key', $this->key ); //如果没有装载session if ( !isset( $this->_CI->session ) ) $this->_CI->load->library('session'); } /** * 设置令牌有效时间 * @param int $second 秒数 */ public function set_token_validtime( $second ) { $this->token_validtime = intval( $second ); } /** * 获取表单令牌有效时间 * @return int 秒数 */ public function get_token_validtime() { return $this->token_validtime; } /** * 验证表单令牌是否合法 * @return bool */ public function valid_token() { if ( $this->debug ) return true; //获取session中的hash $source_hash = $this->_CI->session->userdata( $this->key ); if ( empty( $source_hash ) ) return false; //判断是否超时 if ( $this->is_token_exprie() ) return false; //提交的hash $post_formhash = $this->_CI->input->post( $this->key ); if ( empty( $post_formhash ) ) return false; if ( md5( $source_hash ) == $post_formhash ) { $this->_CI->session->unset_userdata( $this->key ); return true; } return false; } /** * 生成表单令牌(连同input元素) * @return string */ public function create_token( $output = false ) { $code = time() . '|' . $this->get_random( 5 ); $this->_CI->session->set_userdata( $this->key , $code); $result = function_exists('form_hidden') ? form_hidden( $this->key, md5( $code ) ) : '<input type="hidden" name="token" value="'. md5( $code ) .'" />'; if ( $output ) { echo $result; } else { return $result; } } /** * 获取随机数(自己可以扩展) * @param int $number 上限 * @return string */ public function get_random( $number ) { return rand( 0, $number ); } /** * 判断表单令牌是否过期 * @return bool */ public function is_token_exprie() { if ( empty( $this->token_validtime ) ) return false; $token = $this->_CI->session->userdata( $this->key ); if ( empty( $token ) ) return false; $create_time = array_shift( explode('|', $token) ); return ( time() - $create_time > $this->token_validtime ); } } |