之前同事做每周分享时说了thinkphp的令牌,只需要在视图中写上__TOKEN__那么到时就会自动转换成一个隐藏域,顿时觉得很方便于是那时就按照这种思路在CI的基础上扩展了这种产生令牌的方式

记得之前发表过关于hook令牌的两篇文章

如果你还不清楚什么是钩子的话,建议花点时间看看与写写,而令牌其实就是为了防止表单重复提交的,不知道的自己补补

要在CI的基础上实现这种扩展,paperen我首先想到的就是利用钩子,利用在视图输出前的钩子检测文本中是否有__TOKEN__关键字,有则创建一个令牌并用隐藏域替换掉

比如一个这样的视图

<form method="post">
<p><label for="username">username</label><input type="text" name="username" id="username"></p>
<p><label for="password">password</label><input type="text" name="password" id="password"></p>
<!--__TOKEN__-->
</form>

我们要实现的就是获取输出前的文本寻找<!--__TOKEN__-->并将其替换为<input type="hidden" name="token" value="______">

用到的hook为post_controller(关于CI的hooks http://ellislab.com/codeigniter/user-guide/general/hooks.html

post_controller Called immediately after your controller is fully executed

当控制器执行完后马上执行该hook,于是我们就在application/config/hooks.php中对该钩子点进行配置

$hook['post_controller'][] = array(
 'class' => 'MY_Form_validation',
 'function' => 'create_token',
 'filename' => 'MY_Form_validation.php',
 'filepath' => 'libraries',
);

将其钩子注册到libraries/MY_Form_validation.php中的create_token方法,也就是凡是到控制器执行完毕就会出发该MY_Form_validation中的create_token方法

而这个create_token要做的事情也不多

  • 获取将要输出的页面数据
  • 寻找是否有__TOKEN__关键字
  • 有则生成令牌并替换为一个隐藏域

注意:使用令牌必须要session的支持,也要设置一下config.php中的encryption_key 不能为空

MY_Form_validation.php的详细 https://gist.github.com/4346941

稍微解析一下create_token与check_token方法

create_token 生成令牌

通过CI的output类可以获取到将要输出的文本内容 $output = $this->CI->output->get_output();

之后就是在$output这些字符中寻找是否存在<!--__TOKEN__--> (之所以使用html注释的方式是因为保证一旦钩子没有生效不会导致页面莫名出现一个__TOKEN__字符串),存在则产生一个令牌并拼合到一个隐藏域中,最后就是将<!--__TOKEN__-->替换掉 $output = str_replace( self::TOKEN_TAG, $token_html, $output );

/**
 * 生成令牌
 */
public function create_token() {
 $output = $this->CI->output->get_output();
 // 是否存在令牌标识符
 if ( strpos( $output, self::TOKEN_TAG ) !== FALSE ) {
  // 生成并设置令牌
  $time = time();
  $this->set_token( $time );

  // 生成input
  $name = self::TOKEN_SESSION;
  $value = md5( $time );
  $token_html = "<input type=\"hidden\" name=\"{$name}\" value=\"{$value}\">";
  $output = str_replace( self::TOKEN_TAG, $token_html, $output );
 }
 $this->CI->output->set_output( $output );
}


check_token 判断令牌是否正确

思路也简单,只是这里还有一个概念,表单超时时间(表单不能超过指定秒数再提交),暂且不说这个超时时间,而剩下的逻辑也不难理解

是否是调试模式?是 直接返回TRUE好了

session中是否存在token,若有与post过来的token进行比对,符合则进而判断是否超时,若没超时则销毁然后返回TRUE,若有错误则销毁后返回FALSE

判断完后必须要销毁不然会存在漏洞,因为当页面没有重新加载前session中的token仍然没变,而这样的话可以构造一个脚本只要一直拿着原来的token就可以不停post过来了,就像之前12306那个刷票插件一样

在处理数据提交的部分使用$this->form_validation->check_token()即可进行判断


/**
 * 判断令牌是否正确
 * @return bool 正确true/不正确false
 */
function check_token() {
 // 调试模式
 if ( $this->_debug ) return TRUE;

 // 判断是否存在token
 if ( $this->_is_token() ) {
  $now = time();
  $timeout = self::TOKEN_TIMEOUT * 60;
  $token = $this->get_token();
  if ( $token && $this->CI->input->post( self::TOKEN_SESSION ) == md5( $token ) ) {
   // 销毁掉
   $this->destroy_token();

   // 没时间限制
   if ( $timeout == 0 ) return TRUE;
   return ($now - $token) < $timeout;
  }
 }

 // 销毁掉
 $this->destroy_token();
 return FALSE;
}


可以改进的地方

仍有待改进的地方,比如设计得更懒,在项目的配置文件中设置一个参数$config['enabled_token'] = TRUE;那么在输出视图文本前判断是否有form,有则在</form>之前插入隐藏域,但是也支持手动,也就是说如果存在<!--__TOKEN__-->就不会是自动模式(form标签结束之前自动插入),手动优先级高于自动。而记住一个页面只能有一个令牌,所以也就是说若果页面中有多个form的话只能选择其中一个有token