用户
 找回密码
 入住 CI 中国社区
搜索
查看: 27552|回复: 18
收起左侧

[会话/Cookie] CI Session解决方案

    [复制链接]
发表于 2010-1-6 09:59:50 | 显示全部楼层 |阅读模式
本帖最后由 ares333 于 2010-3-10 15:31 编辑

CI Session和PHP原生Session没有本质区别,都是通过发总一个或多个Set-Cookie http头设置客户端Cookie,只不过PHP原生Session会自动完成,CI Session通过setcookie函数完成,当然也可以通过header函数完成。
所以要实现和PHP 原生Session同样的功能就没有必要使用KNDB Session(我看官方wiki上说 支持CI的最高版本是1.6)和其他Session类库,自己稍微扩展一下CI Session即可完成。
我不喜欢第三方类库因为懒得研究其代码(因为CI本身的代码我还没研究所以其他代码不一定能研究通),还有一个原因就是第三方类库不一定能实现和CI 原装类库同样的功能(比如不支持数据库,其他的还没发现)。所以自己扩展一下比较放心。
扩展类库前缀我改了($config['subclass_prefix'] = 'X_';),所以在application/libraries/中新建X_Session.php
PHP复制代码
class X_Session extends CI_Session{
   
    function X_Session(){
        parent::CI_Session();
    }
   
    function _set_cookie($cookie_data = NULL)
    {
        if (is_null($cookie_data))
        {
            $cookie_data = $this->userdata;
        }
 
        // Serialize the userdata for the cookie
        $cookie_data = $this->_serialize($cookie_data);
 
        if ($this->sess_encrypt_cookie == TRUE)
        {
            $cookie_data = $this->CI->encrypt->encode($cookie_data);
        }
        else
        {
            // if encryption is not used, we provide an md5 hash to prevent userside tampering
            $cookie_data = $cookie_data.md5($cookie_data.$this->encryption_key);
        }
 
        // Set the cookie
        //if the sess_expiration set to zero in config file,the Session constructor
        //will reset it to (60*60*24*365*2)
        if($this->sess_expiration == (60*60*24*365*2))
            $expiration = 0;
        else
            $expiration = $this->sess_expiration + time();
        // 添加内容结束
        setcookie(
                    $this->sess_cookie_name,
                    $cookie_data,
                    $expiration,
                    $this->cookie_path,
                    $this->cookie_domain,
                    0
                );
    }
}
复制代码


config文件还是要修改的
PHP复制代码
$config['sess_cookie_name']        = 'session';
$config['sess_expiration']        = 0;
$config['sess_encrypt_cookie']    = FALSE;
$config['sess_use_database']    = FALSE;
$config['sess_table_name']        = 'sessions';
$config['sess_match_ip']        = FALSE;
$config['sess_match_useragent']    = TRUE;
$config['sess_time_to_update']     = 0.5*60*60;
复制代码


只是覆盖了一下CI_Session的一个函数,具体原理请看PHP手册setcookie 函数的第三个参数说明。
我测试了一下:
IE6,打开一个新窗口登陆,登陆成功后关闭浏览器,再打开浏览器发现登陆失效了。如果不关闭浏览器也不退出登陆,新开一个IE窗口,同样需要重新登陆才行。
FF 3.5.6,只有一个窗口(不是标签页)的情况下,和IE相同,两个窗口则共享登陆状态。

这是不使用数据库的情况,如果使用数据库涉及到数据库中无效session的回收,请看3楼

评分

参与人数 2威望 +6 收起 理由
lynn.wang + 1
Hex + 5 原创内容

查看全部评分

发表于 2010-1-6 10:29:08 | 显示全部楼层
改版后的第一篇好文章!加分~
 楼主| 发表于 2010-1-6 11:36:26 | 显示全部楼层
本帖最后由 ares333 于 2010-1-9 22:23 编辑

查阅了很多资料和做了很多测试,能得到这高认可,激动心情无法言语表达。
上面是没有使用数据库的情形,使用数据库后有一个回收问题,CI_Session的_sess_gc()函数。
此函数是在CI_Session的构造函数中最后调用的,如果配置文件中sess_expiration设为0之后,那数据库中存储的session岂不是要两年之后才回收?(因为config文件中设为零,CI会认为这是两年)
解决此问题有两种方法,第一,覆盖CI_Session(就是重写,但是只修改部分代码),第二,就是扩展原有类,覆盖_sess_gc()函数,然后在扩展后的构造函数中调用一次_sess_gc()。
第一种方法条理不如第二种方法清晰,第二种方法相对整体的性能损失几乎为零。所以采用第二种方法:
config.php中添加一项配置(并且增加了几行注释)
PHP复制代码
 
/*
|--------------------------------------------------------------------------
| Session Variables
|--------------------------------------------------------------------------
|
| 'session_cookie_name' = the name you want for the cookie
| 'encrypt_sess_cookie' = TRUE/FALSE (boolean).  Whether to encrypt the cookie
| 'session_expiration'  = the number of SECONDS you want the session to last.
|  by default sessions last 7200 seconds (two hours).  Set to zero for no expiration.
| 'time_to_update'        = how many seconds between CI refreshing Session Information
| 'sess_lifetime'        = session lifetime in database,only work when sess_expiration = 0
|
| caution:
| if sess_expiration = 0 , 'sess_time_to_update' < 'sess_lifetime'
|
*/

$config['sess_cookie_name']        = 'session';
$config['sess_expiration']        = 0;
$config['sess_encrypt_cookie']    = TRUE;
$config['sess_use_database']    = TRUE;
$config['sess_table_name']        = 'sessions';
$config['sess_match_ip']        = FALSE;
$config['sess_match_useragent']    = TRUE;
$config['sess_time_to_update']     = 0.1*60*60;
$config['sess_lifetime'] = 0.5*60*60;
 
复制代码


当sess_expiration = 0 时,sess_lifetime 才起作用,而且'sess_time_to_update' 必须小于'sess_lifetime'
系统运行过程中不能动态改变sess_expiration 的值(也就是说只能在配置文件中一次性定义sess_expiration的值),否则session相关的操作会出现不可预料的结果

X_Session.php

PHP复制代码
<?php
class X_Session extends CI_Session{
       
        function X_Session(){
                parent::CI_Session();
                $this->sess_expiration = $this->CI->config->item('sess_expiration');
                $this->_sess_gc();
        }
       
        function _set_cookie($cookie_data = NULL)
        {
                if (is_null($cookie_data))
                {
                        $cookie_data = $this->userdata;
                }
 
                // Serialize the userdata for the cookie
                $cookie_data = $this->_serialize($cookie_data);
 
                if ($this->sess_encrypt_cookie == TRUE)
                {
                        $cookie_data = $this->CI->encrypt->encode($cookie_data);
                }
                else
                {
                        // if encryption is not used, we provide an md5 hash to prevent userside tampering
                        $cookie_data = $cookie_data.md5($cookie_data.$this->encryption_key);
                }
 
                // Set the cookie
                //sess_expiration = 0 means the cookie won't be effective after the browser closed
                if($this->sess_expiration == 0)
                        $expiration = 0;
                else
                        $expiration = $this->sess_expiration + time();
                setcookie(
                                        $this->sess_cookie_name,
                                        $cookie_data,
                                        $expiration,
                                        $this->cookie_path,
                                        $this->cookie_domain,
                                        0
                                );
        }
       
        function _sess_gc()
        {
                if ($this->sess_use_database != TRUE)
                {
                        return;
                }
               
                srand(time());
                if ((rand() % 100) < $this->gc_probability)
                {
                        if($this->sess_expiration == 0){
                                $expiration = $this->CI->config->item('sess_lifetime');
                        }else{
                                $expiration = $this->sess_expiration;
                        }
                        $expire = $this->now - $expiration;
                               
                        $this->CI->db->where("last_activity < {$expire}");
                        $this->CI->db->delete($this->sess_table_name);
 
                        log_message('debug', 'Session garbage collection performed.');
                }
        }
}
复制代码


_sess_gc()调用了之后不一定会执行(看源代码就会明白),会有一定概率执行(这个概率可以调节),所以如果你的网站配置文件sess_expiration设为0,会产生很多无用的数据项(因为如果sess_expiration不为零,回收时间就是sess_expiration的值,否则是两年),这时候就需要根据实际情况在X_Session中重写_sess_gc()函数调节概率(过高会加重数据库查询负担),这时候回收的概率就是CI系统默认概率+调节后的概率(因为调用了两次回收函数)。这样可以减少session表的体积(尤其对有大量用户的系统)。
把回收概率改为100%简单测试了一下没有问题,希望能给点意见。

参考了一篇文章得到了一些启发。
怕原文失效就贴了过来

http://weblog.kreny.com/2007/06/_php_session_time_out.html
关于 PHP Session 的 Time out 和有效设置 Session 时间限制的一些小结

一直搞不清楚 PHP 里面关于 Session Time Out 的时间控制,这里稍微总结一下,做个纪录。
php.ini 的关于 Session 的设置
① 是否要 cache ?  ==> NO!
在 http 的 header 里面输出一些关于 session 的 cache(??对具体定义不是很清除) ,虽然这个和 Session 的time out 没有什么直接的联系,但从历来的经验来讲,会出现一些很奇怪的现象,所以这里统一将 cache 关闭,使得 header里面不出现 session 的信息。
; Set to {nocache,private,public,} to determine HTTP caching aspects
; or leave this empty to avoid sending anti-caching headers.
session.cache_limiter = nocache
; Document expires after n minutes.
session.cache_expire = 180
② 关于 garbage

; Define the probability that the 'garbage collection' process is started
; on every session initialization.
; The probability is calculated by using gc_probability/gc_divisor,
; e.g. 1/100 means there is a 1% chance that the GC process starts
; on each request.
session.gc_probability = 1
session.gc_divisor     = 100
; After this number of seconds, stored data will be seen as 'garbage' and
; cleaned up by the garbage collection process.
session.gc_maxlifetime = 1440
这个设置是指:在 1440 秒后, Session 会被认作是 garbage (垃圾),而针对这些garbage ,在每次连接(request),有 1/100 的几率(下文称为“清除几率”)来清除这些 garbage--当然,可以认为在1440 秒后,平均通过进行 100 次的连接就会将 garbage session清除。
如果将以上的分子和分母都调整到 1, 那所有的 garbage session 将会在期限(session.gc_maxlifetime)过后的第一次 request 的时候被清除。但是这会大大加大对服务器的负荷,不建议使用。
                                                                                                                                         
③最后设置  session.cookie_lifetime
; Whether to use cookies.
session.use_cookies = 1
; Lifetime in seconds of cookie or, if 0, until browser is restarted.
; session.cookie_lifetime 以秒数指定了发送到浏览器的 cookie 的生命周期。
; 值为 0 表示“直到关闭浏览器”。默认为 0。
session.cookie_lifetime = 0

如果使用 cookie 在客户端保存 session 信息,这里可以设置 cookie 的有效时间。自己的理解是,在客户端的 cookie 里面保存有 Session ID,用来比较其有效期限。
现在做的项目使用 cookie 来保存用户信息,每当 session.cookie_lifetime 设置为 0 的时候,理所当然 IE被关闭后每次都要重新登陆。但是当将此设置为一定的值后,在指定的秒数内重新打开该页面,仍旧保留以前的 cookie 数据并且自动登陆。留意点:
session.cookie_lifetime 设定为一个值 (例如 1000 秒)后,相应的 session.gc_maxlifetime 也需要设置为 1000 ,才能方便清除?
因为测试过几次,将 session.cookie_lifetime 设置为较小的数值(例如 100秒)的时候,即使将“清除几率”变为 100%, 也不能在这个较小数值的时间内将 session 清除。因此怀疑是 session.gc_maxlifetime设置了较大的一个值,使得系统需要更多的时间将 session 认定为 garbage, 即使这个 session 实质上已经过了它的lifetime。 
几经测试,在统一了 session.cookie_lifetime  和 session.gc_maxlifetime 后, session 的定期清除效果比较好。
那么如何有效的限制 Session 的 Time out 呢?以下是两个众所周知的方案,仅作纪录。
  • 将 Timestamp 放入 $_SESSION 中,每次 request 的时候对其值和现在值 now()进行比较。如果时间间隔没有超出 Time Out 的期限,那么就将现在的时间 now() 赋值给 $_SESSION 中的纪录timestamp 的部分。
  • 将 timestamp 放入 数据库中,每次 request 的时候对其值和现在值 now() 进行比较。如果时间间隔没有超出 Time Out 的期限,那么就将该用户的 session 纪录进行更新。
发表于 2010-1-6 14:59:46 | 显示全部楼层
真不错!
发表于 2010-3-1 00:14:30 | 显示全部楼层
本帖最后由 sking 于 2010-3-1 23:38 编辑

你好,这个"CI Session完美解决方案"的帖子,感觉很不错,不过有些疑惑的地方要请教一下:

1、按照你给出的方法,我现在关闭浏览器就可以销毁之前的session,但是不能删除数据库中的记录,数据库中的记录只有执行$this->session->sess_destroy()时才会删除(已经按照你的方法,扩展原有类,覆盖_sess_gc()函数,然后在扩展后的构造函数中调用一次_sess_gc())。是否是因为你所说的,需要在X_Session中重写_sess_gc()函数调节概率,但是这个概率如何调节?要修改哪句代码?可否说的具体些?

2、config中'sess_lifetime'设了好像没用啊

谢谢!!:)
 楼主| 发表于 2010-3-4 16:35:02 | 显示全部楼层
本帖最后由 ares333 于 2010-3-4 16:52 编辑

1._sess_gc()函数会删除数据库中的无用记录,但只是有一定概率会删除,原装的_sess_gc()函数在/system/libraries/Session.php 最后面,
PHP复制代码
        /**
         * Garbage collection
         *
         * This deletes expired session rows from database
         * if the probability percentage is met
         *
         * @access      public
         * @return      void
         */

        function _sess_gc()
        {
                if ($this->sess_use_database != TRUE)
                {
                        return;
                }
 
                srand(time());
                if ((rand() % 100) < $this->gc_probability)
                {
                        $expire = $this->now - $this->sess_expiration;
 
                        $this->CI->db->where("last_activity < {$expire}");
                        $this->CI->db->delete($this->sess_table_name);
 
                        log_message('debug', 'Session garbage collection performed.');
                }
        }
复制代码

有这么一行if ((rand() % 100) < $this->gc_probability),(rand()%100)会产生一个0-99的随即数,$this->gc_probability在上面被初始化为5,只有(rand()%100)产生的随机数小于5的时候才会删除数据库中的记录,把$this->gc_probability的值改为100就会100%删除数据库中的记录,我就不测试了(还得配置环境 )。
2.sess_lifetime用在了扩展后的_sess_gc()函数中
PHP复制代码
if($this->sess_expiration == 0){
                                $expiration = $this->CI->config->item('sess_lifetime');
                        }else{
                                $expiration = $this->sess_expiration;
                        }
复制代码

作用是这样的,如果sess_expiration=0,如果用户在sess_lifetime时间内没有任何操作就是有一定概率删除数据库中的session,其实作用就是为了避免产生数据库垃圾。

如果错误请指正,感觉这一部分挺乱

折腾了半天终于感觉CI的Session真垃圾,应该被和谐
发表于 2010-3-5 23:09:23 | 显示全部楼层
支持原创
学习了!
发表于 2010-4-25 20:33:11 | 显示全部楼层
呵呵 我是新手,多多学习 一直不知道如何使用session
发表于 2010-5-21 10:07:24 | 显示全部楼层
我用了楼主的方法后,在Windows的Wamp环境下很好使,但是放到服务器上就不行了,
关闭浏览器还是能记住登录情报。。。
不知道高手有什么建议没?

作为开发人员我不太理解为什么CI设计Session时要这样做。。
 楼主| 发表于 2010-5-21 10:23:24 | 显示全部楼层
你应该用Session验证登录吧,放到服务器上之后执行一次$this->session->sess_destroy()应该就好了,如果不行,IE8的话按F12有一个开发人员工具,好像是缓存菜单里面有个清除域上的Cookie,打开服务器上的网站地址清除一下Cookie试试,还有不同浏览器对cookie的作用域不同,比如www.abc.com和abc.com不能共用一个Cookie。再不行再说。

强烈建议!
使用CI Session不用上面说的那么麻烦,那只是研究研究,实用价值不大
只需修改Session.php中构造函数中的一部分,注释掉
PHP复制代码
 
                if ($this->sess_expiration == 0)
                {
                        $this->sess_expiration = (60*60*24*365*2);
                }
复制代码

这一部分即可,然后把config.php中
$config['sess_expiration']                =  0 ;就可以了。
最好能覆盖Session,新Session和原有的session唯一区别就是上面的4行代码,这样好记

本版积分规则