|
发表于 2009-3-9 18:26:14
|
显示全部楼层
转一篇相关的网文吧。
session保存在数据库,关于性能方面可查看的资料不多。从以往的经验来说,将Session保存在数据库中,初步上是会觉得性能比File要略降。然而就数据库的特点来说,其本质也是二进制文件,所以,从大流量、广义上来说,保存在数据库,反而提高了性能和稳定性。加上Mysql可优化的地方很多,以及实现数据库保存后,将增加分站部署,安全监控等等诸多便利之处,所以长期来说,一个框架还是应该以提供将Session保存到数据库为基础方法的。
对于php保存到session的细节,网络上可搜罗的资料不多,来去都是基本介绍的方法,我手上几本关于php的书,也都是蜻蜓点水。根据最近的实际测试结果表明,仅仅实现session存储到数据库中,仍不足以保障其安全,关键点在php对于session的gc控制。这个在phpchina.com有很翔实的说明,通过设定:session.gc_probability和session.gc_divisor,可以控制gc执行的几率。
然而我们会发现,问题的关键不在于何时执行gc,因为无论是基于数据库还是File,面向更广的应用时,操作的数量都会呈几何增长,增加系统的gc执行率,无疑增加了系统的负担。那么不执行gc会导致什么问题发生呢?比如我们考虑以下的基本session模式:
session表的基本字段:sid,value,expire,session控制方法(请参照一些最基本的示范,我这里就不罗嗦了),实现最基本的open, close, find, write, destory, gc。
大多数的教程中,会建议我们,在find的方法中执行类似以下的一个SQL CMD:
select value from sessions where sid = ? and expire > ?
那么,我们可以通过一些办法设置expire是否过期,比如,getExpire()返回一个具体的有效期。现在问题的关键是,大多数的教程告诉我们,我们应该这样去搜索session表,可是当expire过期的那些session呢?他仍然存放在session表中,而且他还保存了上一次会话的value。当然,这些过期的session并不会让value生效,但对于很多逻辑不够严谨的程序来说,却会造成一个致命的判断漏洞,比如一个图片验证码的实例:
在显示验证码的页面,我们给客户端的链接标识了一个$_SESSION['auth'] = 'xxxx',也许用户打开页面以后,忙别的事情去了,结果产生了session id,并且也将session id和auth的值写入到session表了。也许你设置的有效期是10分钟,也许是更长,但不管怎么样用户的操作是非线性的,总会打破你的预计。在有效期之外,他回到了需要验证码的页面,并且执行了某些程序,触发了后台的验证,那么程序本应该按照我们上面所设定的,执行find方法,并且取出auth的值去比对。然而这时候,php会很残酷的告诉你,没有auth这个索引所对应的值,因为find对于超过有效期外的session,就返回了一个false,或者一个null,或者别的什么了,总之,他没有返回你预期的结果。
也许,你后台验证码检验的代码是这么写的:
if ($_SESSION['auth'] === xxxx)
这时,假如你开启了报错,php会抛出一个警告,然后略过这部分,自顾自的执行下面的程序去。假如很不幸的你没开启报错,结果用户又一次巧妙的“溜”了过去。
这问题的严重性还在于,假如你并没有对php的gc处理设置一个合适的比率,他似乎不会执行去destory,那么这条过期的session一直会存在于你的session表中,用户会一次又一次的从这验证中“溜”过去。
有些介绍的文章,在session执行close的时候执行gc程序,我也对此方法进行的实验,但从整个非线性分布的情况来推测,每个客户端访问都会激活gc清理,gc的清理是以expire > ?为条件进行搜索的,也就是不管是也不是,总要来回一趟,而且一旦有符合条件的,还是批量删除,放在非线性的环境中,实在不敢估量。
我觉得,session的gc清理,可以被作为系统的进程来执行,比如类似RoR的做法。或者可放在网站的后台,进行人为的操作。而尽量减少php执行过程所消耗的时间(毕竟是动态语言,省一分是一分)。
而对于session,其实也仅仅是针对当前sid产生的,我将find的方法进行了一点修改:
select value,expire from sessions where sid = ?
sid一般都会被作为主键和索引,搜索的效率也相对较高。
接着进行判断:
if ($ses['expire'] > getExpire()) # 表示为有效
return $ses['value'];
else { # 表示为无效
# 这里执行destory方法
return false;
}
也许方法早有人想到并提出来,不过可能鄙人水平有限没搜索到,也许还有更好的方法,欢迎指正。 |
|