ajax请求,create进行了令牌验证并销毁了令牌session,再次ajax提交请求后,无法通过令牌验证,有两个解决办法:
1,在Model.class.php核心类中,添加是否销毁令牌session的参数,当是ajax请求时则不销毁session【不太安全】
2,在create销毁session后,再生成新的令牌,将新生成的令牌返回给ajax回调函数,在回调函数中修改页面的令牌,这样再次请求时使用的是最新令牌
此处分享下自己查看思路,如存在何问题望指出:
问题:
表单普通提交使用了令牌验证,当网络慢的时候避免重复提交保存多余数据问题,但是使用ajax提交第二次会报错
原因:
使用ajax将令牌提交后,在控制器中使用create方法验证数据的时候,将会销毁此次令牌,并通过验证保存数据,
但无法产生新的令牌,页面再次提交旧令牌,在create方法验证时则无法通过,所以报错。为什么ajax提交无法产生新令牌?Create方法又是如何验证令牌并销毁令牌的呢?令牌是存贮在哪里呢?带着这些问题,查看源码
分析:
令牌的产生:
Thinkphp框架从程序的开始,请求——控制器处理——页面输出,使用了大量行为,使用标签【类似事件】(一个标签可以带有很多行为)方式执行程序。
当开启令牌的时候,页面就会生成一个name为__hash__的隐藏域,但是我们只使用了form标签,没有添加这个隐藏域,那肯定是系统给添加的,什么时候会添加呢,很大的可能是通过模板替换时。查看thinkphp的行为,里面有一个TokenBuildBehavior的创建令牌的行为,什么时候调用了这个行为呢?因为thinkphp系统行为的执行是在tags中配置的,查看果然是在view_filter这个事件时执行的,从单词可以看出是与视图类很相关的。产看核心 View.class.php
,可以找到是在fetch方法中,即执行display时执行的,确定是模板输出时生成的令牌。
打开创建令牌的类,可以看到令牌的设置,名字 加密等。 生成的令牌是1、请求页面url md5加密后连下划线连接
2、时间加密【这是页面上看到的】。两个加密串分别作为键和值保存在$_session[‘__hash__’]中【如果你没有修改令牌名称】。生成好的令牌,系统会判断是否开启令牌,是否在页面写__hash__这个名称的隐藏域,如果没写则会只能地通过正则匹配form表单来添加令牌
protected $options = array(
'TOKEN_ON' => false, // 开启令牌验证
'TOKEN_NAME' => '__hash__', // 令牌验证的表单隐藏字段名称
'TOKEN_TYPE' => 'md5', // 令牌验证哈希规则
'TOKEN_RESET' => true, // 令牌错误后是否重置
);
'view_filter' => array(
'ContentReplace', // 模板输出替换
'TokenBuild', // 表单令牌
'WriteHtmlCache', // 写入静态缓存
'ShowRuntime', // 运行时间显示
),
令牌的验证:
令牌产生了,那么怎么防止重复提交呢?
Thinkphp使用create方法做了很多工作,其中包括令牌验证,可以通过核心类model.class.php文件查看,但是create方法并不进行真正的写数据库操作,保存工作交给了add等方法。通过 $data = $_POST;将提交的数据交给了create方法,所以使用这个方法时可以不用手动传递参数。将提交的数据进行字符串分隔,分隔好的数据包括了键和值两部分,如果session中保存的数据和提交的数据相符则通过验证并销毁这个session,如果不相符,则返回false并销毁【如果重置令牌为开启状态】。只有通过了create方法的验证才会调用真正的保存操作add,所以如果在还未保存数据时进行了第二次提交,那么session已经消除,验证不通过报令牌错误。
此时就清楚了,ajax请求后,在控制器进行了create验证,销毁了session但是,没有机会产生新的令牌,因为产生新令牌是在输出页面的时候调用的创建令牌的行为。
解决办法:
1,在Model.class.php核心类中,添加是否销毁令牌session的参数,当是ajax请求时则不销毁session【不太安全】2,在create销毁session后,再生成新的令牌,将新生成的令牌返回给ajax回调函数,在回调函数中修改页面的令牌,这样再次请求时使用的是最新令牌
第一种太简单,此处详述第二种办法。
代码参见:
创建只生成令牌不进行模板替换的行为类【最好继承系统的令牌创建行为类,避免自己再次定义一些变量等】,创建自己的行为标签【在tags中添加'create_token' => array('TokenOnly'),】,在create方法验证完了后调用创建令牌的行为类,并将产生的令牌返回给ajax回调函数
$token='';
tag(create_token,$token);
echo json_encode(array('flag'=>"ok",'token'=>$token));
在视图页面进行js修改页面的__hash__隐藏域的值,变为最新令牌
$(__hash__).val(data.token);
就此完成
对于只创建令牌的行为类,继承系统令牌创建行为类,去掉带有标签的内容即可,代码参考:
<?php
defined('THINK_PATH') or exit();
/**
* 只返回生成的表单令牌
*/
class TokenOnlyBehavior extends TokenBuildBehavior {
public function run(&$content){
$this->buildToken(&$content);
}
// 创建表单令牌
private function buildToken(&$content) {
$tokenName = C('TOKEN_NAME');
$tokenType = C('TOKEN_TYPE');
if(!isset($_SESSION[$tokenName])) {
$_SESSION[$tokenName] = array();
}
// 标识当前页面唯一性
$tokenKey = md5($_SERVER['REQUEST_URI']);
if(isset($_SESSION[$tokenName][$tokenKey])) {// 相同页面不重复生成session
$tokenValue = $_SESSION[$tokenName][$tokenKey];
}else{
$tokenValue = $tokenType(microtime(TRUE));
$_SESSION[$tokenName][$tokenKey] = $tokenValue;
}
$content = $tokenKey.'_'.$tokenValue;
}
}