我们的请求通过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.