我们的请求通过TCP/IP协议到达PHP进程之后,指向某个入口文件,然后通过加载一些系统文件,对一些配置进行初始化,比如环境,时间,语言等等,解读url,请求方式,映射到相应的controller,调用相应的method,最后指向到具体的方法,然后在方法中,通过$_POST, $_GET, $_SERVER, $_FILES等魔术变量获取相应的数据,如果是已经有登录态,还可以从session中获取数据。而session的数据,一般是在用户登录后,将一些用户常用到的信息写入session。我们以CI框架为例,来看看是怎么实现的。

一个访问者访问你的 web 网站将被分配一个唯一的 id, 就是所谓的会话 id. 这个 id 可以存储在用户端的一个 cookie 中,也可以通过 URL 进行传递. Session会话用来追踪每个用户的会话,使用服务器生成的SessionID进行标识,用以区分用户。Session存放在服务器的内存中,SessionID存放在服务器内存和客户机的Cookie里面。这样,当用户发出请求时,服务器将用户Cookie里面记录的SessionID和服务器内存中的SessionID进行比对,从而找到这个用户对应的Session进行操作。所以,如果客户机禁止Cookie的话,Session也不能使用。

1, 下面是一个普通的set操作

$this->load->library('session');
$this->session->set_userdata('uid', $userInfo['uid']);

第一行是加载框架下的...../thirdsrc/framework/libraries/Session.php文件

这个文件中包含两个类,一个是class CI_Session,另一个是class memcacheSessionHandler, 文件最后执行了 new memcacheSessionHandler(); 这个初始化命令,我们来看看。

2,  这里是将memcached作为session的handle.

private $host      = "localhost";
private $port      = 11212;
private $lifetime  = 0;
private $memcached = null;
private $session_prefix='memc.sess.key.';

/**
* Constructor
*/
public function __construct(){
$this->CI =& get_instance();
$servers = $this->CI->config->item(ENVIRONMENT,'sess_storage');
foreach (array('host', 'port') as $key){
    $this->$key =  $servers[$key];
}
$this->memcached = new Memcached();

$this->memcached->addServer($this->host, $this->port) ;
$this->memcached->setOption(Memcached::OPT_PREFIX_KEY, $this->session_prefix);
$this->memcached->setOption(Memcached::OPT_COMPRESSION, FALSE);
session_set_save_handler(
        array($this, "open"),
        array($this, "close"),
        array($this, "read"),
        array($this, "write"),
        array($this, "destroy"),
        array($this, "gc")
        );
//log_message('info', sprintf("%s Class Initialized", __CLASS__));
}

3, 然后初始化CI_Session这个类的构造方法如下:

function __construct()
    {
        $this->CI =& get_instance();
        log_message('debug', "Native Session Class Initialized");

        // Set all the session preferences, which can either be set
        // manually via the $params array above or via the config file
        foreach (array('sess_encrypt_cookie', 'sess_use_database', 'sess_table_name', 'sess_expiration', 'sess_expire_on_close', 'sess_match_ip', 'sess_match_useragent', 'sess_cookie_name', 'cookie_path', 'cookie_domain', 'cookie_secure', 'sess_time_to_update', 'time_reference', 'cookie_prefix', 'encryption_key') as $key)
        {
            $this->$key = (isset($params[$key])) ? $params[$key] : $this->CI->config->item($key);
        }

        $this->_sess_run();
    }

实例化,再将配置文件中的一些值赋给对象的属性,然后执行。

再看执行方法:

/**
* Starts up the session system for current request
*/
function _sess_run()
{
//设置session会话名称
session_name($this->CI->config->item('sess_cookie_name'));
//获取session的过期时间
$session_id_ttl = $this->CI->config->item('sess_expiration');
//设置session_id的过期时间
if (is_numeric($session_id_ttl))
{
    if ($session_id_ttl > 0)
    {
        $this->session_id_ttl = $this->CI->config->item('sess_expiration');
    }
    else
    {
        $this->session_id_ttl = (60*60*24*365*2);
    }
}
//将session_id的过期时间设置到php.ini中的session.gc_maxlifetime上(具体这项代表什么意思,我们另外再讨论)
ini_set('session.gc_maxlifetime',  $this->session_id_ttl);
//设置一些cookie的参数
session_set_cookie_params($this->session_id_ttl, $this->CI->config->item('cookie_path'), $this->CI->config->item('cookie_domain'));
//如果不存在session,则开启session
if(!isset($_SESSION)) {
    session_start();
}
//如果session_id过期了,重新生成session_id
// check if session id needs regeneration
if ( $this->_session_id_expired() )
{
    log_message('WARN', 'session has expired');
    $this->regenerate_id();
}
//刷新数据
$this->_flashdata_sweep();

// mark all new flashdata as old (data will be deleted before next request)
$this->_flashdata_mark();
}

我们再来看看重新生成session_id这个方法

/**
* Regenerates session id
*/
function regenerate_id()
{
//复制旧session数据
// copy old session data, including its id
$old_session_id = session_id();
$old_session_data = $_SESSION;
//重新生成session_id并保存
// regenerate session id and store it
session_regenerate_id(TRUE);
$new_session_id = session_id();
//切换到旧session_id并将它销毁
// switch to the old session and destroy its storage
session_id($old_session_id);
// @session_destroy();
$this->sess_destroy();
//再切回新session并设置cookie
// switch back to the new session id and send the cookie
session_id($new_session_id);
	session_set_cookie_params($this->session_id_ttl , $this->CI->config->item('cookie_path'), $this->CI->config->item('cookie_domain'));
//开启session
session_start();
//将旧session中的数据重新赋值给到新session中
// restore the old session data into the new session
$_SESSION = $old_session_data;
//$_SESSION   = array();
//更新session的创建时间
// update the session creation time
$_SESSION['regenerated'] = time();
$_SESSION['ip_address'] = $this->CI->input->ip_address();

// session_write_close() patch based on this thread
// http://www.codeigniter.com/forums/viewthread/1624/
// there is a question mark ?? as to side affects

// end the current session and store session data.
session_write_close();
}

看到这里,有一个基本概念,就是$_SESSION里面存的是一些session的信息,如下:

但是通过session_name()设置的会话名称,session_id()生成并设置的会话ID,并不在其中。

array (size=10)
  'regenerated' => int 1544077095
  'uid' => int 81
  'uname' => string 'cage1618@qq.com' (length=15)
  'companyName' => string '涓浗绉诲姩閫氫俊闆嗗洟璁捐闄㈡湁闄愬叕鍙�' (length=45)
  'companyLogo' => string '/default-company-logo.png' (length=25)
  'subUser' => int 0
  'accountType' => int 1
  'isVanke' => int 0
  'tob_csrf_token_2' => string '783ead945bfe776936c96f796e6bff37' (length=32)
  'ip_address' => string '127.0.0.1' (length=9)

同时,session_id是用a-z, A-Z, 数字, 逗号,减号组成的,长度为26。如果是在sesson_id()方法中传入了参数,则表示设置当前会话的session_id,这样的话,在调用session_id()这个方法之前,应该先调用session_start()这个方法。

以php5.3.6的源码为例,进入/ext/session目录,生成session id的函数位于session.c文件的345行,c语言函数原型如下:

PHPAPI char *php_session_create_id(PS_CREATE_SID_ARGS);

有兴趣的可以具体分析实现原理。

PHP默认的session id长度

截取一些实际的php 5.4.6服务端生成的session id如下:

sess_00nrqa20hjrlaiac0eu726i4q5      sess_89j9ifuqrbplk0rti2va2k1ha0      sess_g2rv1kd6ijsj6g6c9jq5mqglv5  
sess_04es72a83tqsl0jqd3cvrc4s01      sess_8b7a5lme60g49lvk4u4jiemdn1    sess_g3uk6d3gbashg5eoq0b2k7vsk0  
sess_04u0ns0oobh2g93t009bij2rq0    sess_8dfvkiv8ml44fdqrk1rcmjchs4       sess_g64tddhbo8pbj8bs7bel44rf35  
sess_0592dolr5m0k392fah6c9preg7    sess_8fhgkjuakhatbeg2fa14lo84q1      sess_g6kl828qqsnvdrse7ff52cl2a4  
sess_066g8irr0m22iqotscepub4e13     sess_8gn03i9j1tta7655qfj6nl1l53       sess_g8t45j6qce7mf55nk14cotj5i4  
sess_08nr1fav9jqs2pdi5qlpsmd247     sess_8gvu05313o7p9usksaacaiegu6    sess_gbtjmr57iat86c8ve86ar5nh30

可见具体的session_id 为 “sess_”后面的部分,长度为26位,此长度仅限于php 5.4.6.

session_id 看似多次刷新都不会改变其实是没有删除本地相关联的cookie。如果session_id的cookie存在并且未过期,则不重新生成session_id,否则需要重新生成。

种植在客户端浏览器中的session_id 会出现重复吗?
session_id 安全性如何,有没可能被黑客轻易的仿造呢?
带上这个问题,我稍微注意了一下PHP的源码后,疑问也就有了答案。

在PHP在使用默认的 session.save_handler = files 方式时,session_id 的生成算法原理如下:

hash_func = md5 / sha1 #可由php.ini配置
PHPSESSIONID = hash_func(客户端IP + 当前时间(秒)+ 当前时间(微妙)+ PHP自带的随机数生产器)

从以上hash_func(*)中的数据采样值的内容分析,多个用户在同一台服务器时所生产的PHPSESSIONID重复的概率极低(至少为百万份之一),设想,但台动态Web Server能到2000/rps已经很强悍了。

另外,黑客如果要猜出某一用户的PHPSESSIONID,则他也必须知道“客户端IP、当前时间(秒、微妙)、随机数”等数据方可模拟。

4, 将session_id保存进memcache, 代码如下:

//保存用户登录记录session id和time
$sessionID = $this->session->userdata('session_id');
$this->load->helper('tob_session');
saveUserSessionID($sessionID, $userInfo['uid']);

这里先获取产生的sessionId,然后调用tob_session_help.php文件下的saveUserSessionID()方法,

if ( ! function_exists('saveUserSessionID')){
    function saveUserSessionID($id, $uid){
        $CI =& get_instance();
        $CI->load->driver('cache', NULL, 'cache');
        $mkey = 'tob_user_sessionid_' . $uid;
        $userSessionArray = array('sess_id' => $id, 'login_time' => time());
        do {
            $userSessionArrayResult = $CI->cache->memcached->getCas($mkey, null ,$cas);

            if ($CI->cache->memcached->getResultCode() == Memcached::RES_NOTFOUND) {
                $userSessionArrayResult = array();
                array_push($userSessionArrayResult, $userSessionArray);
                $CI->cache->memcached->add($mkey, $userSessionArrayResult);
            } else {
                if(!is_array($userSessionArrayResult)){
                    $userSessionArrayResult = array();
                }
                array_push($userSessionArrayResult, $userSessionArray);
                $newArr = array();
                foreach ($userSessionArrayResult as $val) {
                    $newArr[$val['sess_id']] = $val;
                }
                $userSessionArrayResult = array_values($newArr);
                $CI->cache->memcached->cas($cas, $mkey, $userSessionArrayResult);
            }
        } while ($CI->cache->memcached->getResultCode() != Memcached::RES_SUCCESS);
    }
}

可以看到它使用了一个默认的key的前缀_uid的方式来生成key,存入memcache, 我们用脚本调用memcache内容之后,结果如下:

$key = 'tob_user_sessionid_81';
var_dump($m->get($key));

结果:

/home/c80k2/桌面/脚本/memcache.php:84:
array(2) {
  [0] =>
  array(2) {
    'sess_id' =>
    string(26) "p5v98siqilhl3n8o9qc9tvgq16"
    'login_time' =>
    int(1544087215)
  }
  [1] =>
  array(2) {
    'sess_id' =>
    string(26) "1efsptakr324fu23q9lp66koi3"
    'login_time' =>
    int(1544084142)
  }
}

OK.

12-07 08:26