看着博客好久没人留言评论,今天心血来潮怀疑是不是我的博客评论系统坏了,然后随便给自己的一篇文章回复,发现竟然点击提交后竟然直接页面刷新,写的评论内容全部消失了...果然是坏了...
不过奇怪的是在把文章页面刷新后再评论就一切正常了...然后今天就花了一整天时间来找这个bug,所幸最终还是找到并成功解决了这个bug,问题出在Ajax请求的参数上。趁着自己还没有忘记,赶紧记录下来。
问题描述
博客是基于Typecho的,但主题是自己写的,为了体现出水平(作死),全站绝大部分内容都是采用Ajax加载。
症状就是:
如果是从博客中的各种链接点击进入文章页面的话,直接评论点击提交,得到的结果就是页面刷新了一次,然后评论的内容完全消失,就像从来没发生过;
在刷新后的文章页面上,评论点击提交,一切工作正常,页面刷新并显示刚才评论的内容;
从博客中的各种链接点击进入文章页面,并手动刷新一次再评论,会和2一样一切正常;
直接通过网址进入文章页面,评论一切正常。
原因分析
这里就简单点说了,实际是我花了一天才找到问题在哪。
怀疑点当然是在Ajax上了,但一般来说评论不成功Typecho都会返回一个页面解释原因的,但现在的情况是完全没有任何反馈直接刷新页面。开始手动慢慢找,首先从评论失败和评论成功两种文章页面的差异上下手,在Typecho设置->评论设置中关掉了开启反垃圾保护后终于发现了不同。
两个页面在评论表单中
XML
<form method="post"
action="https://www.ozabc.com/archives/Python-%E5%AE%9E%E7%8E%B0%E5%85%A8%E6%8E%92%E5%88%97.html/comment?_=d59336438aae02e5ab577dfc7f147bf3"
id="comment-form" role="form" class="form-horizontal">
后面的d59336438aae02e5ab577dfc7f147bf3这个串的值不一样,怀疑就是因为Ajax加载的文章这个串值是错的,导致提交评论时直接被丢弃。为了验证猜想开始扒源码。
主题中comments.php这部分的源码是
<form method="post" action="<?php $this->commentUrl() ?>"
id="comment-form" role="form" class="form-horizontal">
$this->commentUrl()在Typecho的Widget/Archive.php下
PHP
protected function ___commentUrl()
{
/** 生成反馈地址 */
/** 评论 */
$commentUrl = parent::___commentUrl();
//不依赖js的父级评论
$reply = $this->request->filter('int')->replyTo;
if ($reply && $this->is('single')) {
$commentUrl .= '?parent=' . $reply;
}
return $this->options->commentsAntiSpam ? $commentUrl : $this->security->getTokenUrl($commentUrl);
}
commentsAntiSpam就是反垃圾评论了,关了这个开关执行的就是getTokenUrl($commentUrl),看来那个串值就叫token。
$this->security->getTokenUrl()在Typecho的Widget/security.php下
public function getTokenUrl($path)
{
$parts = parse_url($path);
$params = array();
if (!empty($parts['query'])) {
parse_str($parts['query'], $params);
}
$params['_'] = $this->getToken($this->request->getRequestUrl());
$parts['query'] = http_build_query($params);
return Typecho_Common::buildUrl($parts);
}
public function getToken($suffix)
{
return md5($this->_token . '&' . $suffix);
}
果然就是这加的_这个参数,getToken()似乎就是一个加盐的MD5加密,所以现在来关注getRequestUrl()。
$this->request->getRequestUrl()在Typecho的Typecho/Request.php下
public function getRequestUrl()
{
return self::getUrlPrefix() . $this->getRequestUri();
}
public function getRequestUri()
{
if (!empty($this->_requestUri)) {
return $this->_requestUri;
}
}
看来问题就在_requestUri上了,把两个文章页面上的这个值打印出来看看
XML
https://www.ozabc.com/archives/Python-%E5%AE%9E%E7%8E%B0%E5%85%A8%E6%8E%92%E5%88%97.html?load_type=ajax
https://www.ozabc.com/archives/Python-%E5%AE%9E%E7%8E%B0%E5%85%A8%E6%8E%92%E5%88%97.html
出错的页面后面多了个?load_type=ajax。
先别着急得出结论,看看为啥这样会造成页面刷新。因为URI不同,所以MD5加密得到的token也不同。看看Typecho怎么处理评论的。
处理评论是在Typecho的Widget/Feedback.php下
private function comment()
{
// 使用安全模块保护
$this->security->protect();
...
}
看到对评论首先就用protect()做一次验证,看看这个是在干啥。
$this->security->protect()在Typecho的Widget/security.php下
public function protect()
{
if ($this->request->get('_') != $this->getToken($this->request->getReferer())) {
$this->response->goBack();
}
}
找到出错的地方啦!这里把评论POST请求的_参数(即token)与请求的Referer(HTTP请求中的参数)比较,如果不一致就返回请求页面,效果就是页面刷新啦!通过HTTP抓包可以看到Referer就是发送POST的页面,即文章的URI;与_参数的区别就在于没有?load_type=ajax这部分了,这就是原因了。这个protect()就是防止恶意请求的安全模块,检查请求页面是否一致。
那么?load_type=ajax是从哪来的呢?相信熟悉Ajax的都比较熟悉:为了能够让服务器端知道请求是Ajax请求,在客户端发送Ajax GET请求时添加一个参数用来指示请求类型。这是一个常用的方法,但在Typecho这就因为URI不一致被安全模块挡回去了。
看看客户端发送Ajax请求
JavaScript
$.ajax({
type:'get',
url:$('#logo').attr('href') + "page/" + current_page+"/",
data:{'load_type':'ajax'},
success:function(msg){
...
}
});
其中data就是GET请求参数,手动加上了load_type。
再看看服务器端处理
<?php
if(isset($_GET['load_type']) and $_GET['load_type'] == 'ajax'){
...
}
?>
验证GET请求参数,判断是否为Ajax。怎么样,是不是很常见?
解决方法
通过上面的分析,原因一目了然啦,就是自己添加GET参数使得请求被安全模块过滤掉了。现在有两个选择,一是修改Typecho代码,绕过安全模块(显然只要把protect()那行注释掉就行了);另一个就是换用其他的服务器端Ajax识别机制,不要通过GET参数了。方法一肯定不能选了,那是拣了芝麻丢了西瓜,尝试方法二。
运气很好,很快就找到了一种新的方法,比之前通过GET参数不知道高到哪去了。对于Ajax请求,绝大部分客户端框架在发送这种请求时,都会发送HTTP_X_REQUESTED_WITH这个HTTP头,并且值为XMLHttpRequest。这样,客户端不需要对Ajax请求作任何标记,只需要服务器端验证HTTP_X_REQUESTED_WITH就能判断是否是Ajax请求了。
另一个发现就是Typecho实际上已经提供了这个API。在Typeco/Request.php中
public function isAjax()
{
return 'XMLHttpRequest' == $this->getServer('HTTP_X_REQUESTED_WITH');
}
isAjax()用来判断此请求是不是Ajax请求。
所以只需要将客户端Ajax请求GET参数去掉
JavaScript
$.ajax({
type:'get',
url:$('#logo').attr('href') + "page/" + current_page+"/",
success:function(msg){
...
}
});
然后服务器端验证修改为
<?php
if($this->request->isAjax()){
...
}
?>
怎么样?是不是健壮了许多?
小结
鉴于现在越来越多的框架采用了如Typecho的安全验证手段,在我们DIY Ajax请求时也要注意这方面潜在的问题。
潜在问题
没错,使用HTTP_X_REQUESTED_WITH也有潜在问题,不是所有的框架都支持Ajax请求设置HTTP_X_REQUESTED_WITH,比如早期版本的jQuery和Dojo。不过这个问题也容易解决,自己在Ajax请求构造HTTP_X_REQUESTED_WITH头就好了,只是需要注意这点。
如若转载,请注明出处:https://www.ozabc.com/jianzhan/16310.html