标签归档:会话

Laravel Session详解

Laravel中Session的实现并没有使用PHP本身的Session扩展。而是自己实现了一套Session实现。好处是灵活,不好是不兼容使用PHP原生Session实现的应用。

Session是一个服务,当然是由SessionServiceProvider.php引入框架的:

<?php
namespace Illuminate\Session;
use Illuminate\Support\ServiceProvider;

class SessionServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->registerSessionManager();

        $this->registerSessionDriver();

        $this->app->singleton('Illuminate\Session\Middleware\StartSession');
    }

    protected function registerSessionManager()
    {
        $this->app->singleton('session', function ($app) {
            return new SessionManager($app);
        });
    }

    protected function registerSessionDriver()
    {
        $this->app->singleton('session.store', function ($app) {
            $manager = $app['session'];
            return $manager->driver();
        });
    }
}

注册了一个session,对应SessionManager实例,session.store是$manager->driver()返回的实例,它是Session存在的默认配置。关于Manager的实现都是老套路。最后关键的是绑定了一个Illuminate\Session\Middleware\StartSession。

在框架启动流程中:

namespace App\Http;
 
use Illuminate\Foundation\Http\Kernel as HttpKernel;
 
class Kernel extends HttpKernel {
    protected $middleware = [
        'Illuminate\Session\Middleware\StartSession',
    ];
 
}

这里的$middleware就是定义路由前需要调用的中间件,所以Session的逻辑就在这里被勾进来的。大概看一下这个中间件的hanlder方法:

    public function handle($request, Closure $next)
    {
        $this->sessionHandled = true;

        // If a session driver has been configured, we will need to start the session here
        // so that the data is ready for an application. Note that the Laravel sessions
        // do not make use of PHP "native" sessions in any way since they are crappy.
        if ($this->sessionConfigured()) {
            $session = $this->startSession($request);

            $request->setSession($session);
        }

        $response = $next($request);

        // Again, if the session has been configured we will need to close out the session
        // so that the attributes may be persisted to some storage medium. We will also
        // add the session identifier cookie to the application response headers now.
        if ($this->sessionConfigured()) {
            $this->storeCurrentUrl($request, $session);

            $this->collectGarbage($session);

            $this->addCookieToResponse($response, $session);
        }

        return $response;
    }

首先启动Session,然后调用$request->setSession($session),这样$request->session()就可用了。接下来存储当前URL,垃圾回收,添加会话Cookie到响应。从这里来看,每次响应都应该有一个setCookie的响应头。

简单来说,每次都会从客户端cookie中取回Session ID,由于这个Session ID由Cookie传输,其加解密工作由Cookie完成,如果接收不到这个ID就启动一个新的ID。然后使用这个ID,通过Handler类提供的方法,把存在的数据load回来(没有就是空),产生会话token(如果没有的话),标记会话启动。

不同的Driver实际上就是对应应用了不同Hander的Store对象(以上的描述的过程就是在Store中完成的),Store应用Hanlder方法取回和保存数据,所以如果要实现把Session数据保存到其它介质,只要实现Hanlder接口即可。另外,保存的Session数据是可以做加密保存的,这个取决于conf/session.php中的配置。

对Session的操作的方法,都是由Store对象提供的,可以通过$request->session()获取到SessionManager,也可以使用全局的方法session(),或者app(“session”)都是可以的,不过需要之一,这里返回的都是一个SessionManager实例,然而可以通过它间接调用默认Driver(Store对象)的方法,app(‘session.store’)才是真正对应Store对象。

用法:

$value = $request->session()->get('key', 'default');

$value = $request->session()->get('key', function() {
    return 'default';
});

$data = $request->session()->all();

#使用session()
Route::get('home', function () {
    // 从session中获取数据...
    $value = session('key');

    // 存储数据到session...
    session(['key' => 'value']);
});

if ($request->session()->has('users')) {
    //
}

$request->session()->put('key', 'value');

$request->session()->push('user.teams', 'developers');

$value = $request->session()->pull('key', 'default');

$request->session()->forget('key');
$request->session()->flush();

$request->session()->regenerate();

$request->session()->flash('status', 'Task was successful!');

$request->session()->reflash();
$request->session()->keep(['username', 'email']);

另外一个比较坑的地方,估计就是会话数据的存储了,从源代码来看,Session数据视乎是在发送前些前才一次性把会话数据同步到存储介质,那么就是说如果中间异常退出,那么之前保存的数据就无法持久化。

PHP SESSION 垃圾回收机制

PHP SESSION 垃圾回收机制涉及三个参数(在php.ini中设置):

session.gc_probability = 1
session.gc_divisor = 100
session.gc_maxlifetime = 1800

会话保存在服务器端,它的有效时间由session.gc_maxlifetime决定,比如会话A超过了1800秒不操作,那么这个会话就已经过期(属于垃圾),过程的会话不是一旦过期就会被清理的,它需要等待垃圾回收器执行。这里涉及到一个垃圾回收器触发的问题。PHP执行脚本完毕后就可以认为对应的进程退出,它不是长驻内存的,所以垃圾回收器的触发必定是在会话启动时去触发,但是如果每次会话都启动垃圾回收清扫一遍垃圾,显然很不现实。这就是session.gc_probability和session.gc_divisor的作用。

简单来说,session.gc_probability和session.gc_divisor决定了在每次启动会话时,垃圾回收器可能被触发的概率。计算公式是:session.gc_probability/session.gc_divisor,对以上参数的配置,这个概率是百分之一。换个通俗的说法就是每次会话开始(session_start())时,垃圾回收器有百分之一的概率被触发,一旦触发,那些过期的会话(文件等)就会被清理。

从原理上,垃圾回收器触发后,扫描一遍会话文件(假设会话使用文件存储),对每个扫描的文件,判断它是否过期,如果过期就删除。所以,如果会话很多,而且垃圾回收器被触发的概率又很大,那么产生的IO就会上升,甚至影响到系统性能。最简单的办法是调大session.gc_divisor值,这样垃圾回收器被触发的概率就会变小,如果把会话保存在共享内存中,则可以适当调大回收器被触发的概率,因为内存的读写速度比硬盘自然快很多。

另外,session.cookie_lifetime的设置实际跟SESSION的在服务端的保存没有关系。在SESSION和客户端使用cookie进行交互时,它的设置影响到这个cookie的生存时间,如果设置为0,说明浏览器关闭时,这个cookie被删除,但是这个cookie关系的SESSION是否被清理和它没有关系。

在session.cookie_lifetime设置为0的情况下,如果浏览器不退出,你可能碰到登录了很久,但是都不超时的情况。这个情况可能是,一,访问量少;二,垃圾回收器触发概率太小;使得垃圾回收器没有被触发过。如果要严格进行超时控制,单纯依靠PHP的SESSION机制是不行的,我们可以在登录时,把登录的时间记录一下,第二次操作这个会话时,判断一下是否超时,超时就直接清空会话,定位到登录页。

永久链接:http://blog.ifeeline.com/1596.html

Zen-cart中SESSION的配置与封装

关于SESSION的配置,对应后台Configuration -> Sessions:
Zen-cart会话配置

相关配置对应的常量定义如下:
Zen-cart会话配置对应的常量定义

除了Cookie Domain对应SESSION_USE_FQDN外,其它都是见名知意的。如下:

SESSION_WRITE_DIRECTORY          当使用文件来存储会话内容时,它用来控制存储的目录,不过再150以后的版本只能使用数据库来存储了,这个参数无用了
SESSION_USE_FQDN                 这个是设置会话cookie的域名,它可以控制添加的cookie是否是FQDN,其实如果域名是访问网址是www.ifeeline.com,这个变量设置为Ture时就是www.ifeeline.com,是False是就是ifeeline.com
SESSION_FORCE_COOKIE_USE         是否强制使用cookie来传递会话ID
SESSION_CHECK_SSL_SESSION_ID     是否检查SSL会话ID
SESSION_CHECK_USER_AGENT         是否检查用户浏览器前后是否一致
SESSION_CHECK_IP_ADDRESS         是否检查用户IP前后是否一致
SESSION_BLOCK_SPIDERS            是否阻止机器人会话(如果强制使用cookie,则此设置没有使用)
SESSION_RECREATE                 是否更换会话ID(比如登录后更换ID)
SESSION_IP_TO_HOST_ADDRESS       是否把客户端的IP转换成名字(将发起DNS查询,建议不要开启)
SESSION_USE_ROOT_COOKIE_PATH     是否使用跟作为会话cookie的路径参数(默认为False,将根据程序实际情况自己决定)
SESSION_ADD_PERIOD_PREFIX        是否添加域名前缀(点号)到会话cookie的域名设置参数

关于SESSION_USE_FQDN设置,主要初始化文件在includes/init_includes/init_tlds.php中:

// 主要代码,zen_get_top_level_domain()函数受到SESSION_USE_FQDN设置影响
$http_domain = zen_get_top_level_domain(HTTP_SERVER);
$https_domain = zen_get_top_level_domain(HTTPS_SERVER);
$cookieDomain = $current_domain = (($request_type == 'NONSSL') ? $http_domain : $https_domain);

测试zen_get_top_level_domain()函数,假如当前网址是www.ifeeline.com,当SESSION_USE_FQDN是True时,输出是www.ifeeline.com,当SESSION_USE_FQDN是False时,输入ifeeline.com。这个设置主要影响到会话cookie的域设置,如果SESSION_USE_FQDN为Flase时,带www的网址www将被去掉:
会话域设置

关于SESSION_RECREATE设置,主要在登录和退出登录时用到。

其它的设置主要在includes/init_includes/init_sessions.php中,以下为代码逻辑(删除了部分代码,添加了详细注释)

require(DIR_WS_FUNCTIONS . 'sessions.php');
// 设置会话名称
zen_session_name('zenid');
// 对应后台Session Directory 设置保存路径,不过只是使用文件保存会话内容时 不过1.5.0以后只采用数据库保存
zen_session_save_path(SESSION_WRITE_DIRECTORY);

// 准备设置会话cookie的参数 前台所有页面都是通过访问index.php展示的,所有$_SERVER['SCRIPT_NAME']永远都是/index.php, 如果网站是http://sample.com/zcc,那么就是/zcc/index.php
$path = str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME']));
// 对应后台的 Use root path for cookie path,默认是False的,比如设置了访问网站是http://sample.com/zcc,那么将使用/zcc作为会话cookie的路径
if (defined('SESSION_USE_ROOT_COOKIE_PATH') && SESSION_USE_ROOT_COOKIE_PATH  == 'True') $path = '/';
$path = (defined('CUSTOM_COOKIE_PATH')) ? CUSTOM_COOKIE_PATH : $path;
// 对应后台的 Add period prefix to cookie domain
$domainPrefix = (!defined('SESSION_ADD_PERIOD_PREFIX') || SESSION_ADD_PERIOD_PREFIX == 'True') ? '.' : '';
// 这句代码是150版本以后新加 目的是当网站全站使用SSL时,设置cookie也只使用SSL来发送
$secureFlag = ((ENABLE_SSL == 'true' && substr(HTTP_SERVER, 0, 6) == 'https:' && substr(HTTPS_SERVER, 0, 6) == 'https:') || (ENABLE_SSL == 'false' && substr(HTTP_SERVER, 0, 6) == 'https:')) ? TRUE : FALSE;
// $cookieDomain在includes/init_includes/init_tlds.php中
if (PHP_VERSION >= '5.2.0') {
  	session_set_cookie_params(0, $path, (zen_not_null($cookieDomain) ? $domainPrefix . $cookieDomain : ''), $secureFlag, TRUE);
} else {
  	session_set_cookie_params(0, $path, (zen_not_null($cookieDomain) ? $domainPrefix . $cookieDomain : ''), $secureFlag);
}

if (isset($_POST[zen_session_name()])) {
  	zen_session_id($_POST[zen_session_name()]);
} elseif ( ($request_type == 'SSL') && isset($_GET[zen_session_name()]) ) {
  	zen_session_id($_GET[zen_session_name()]);
}

$ipAddressArray = explode(',', $_SERVER['REMOTE_ADDR']);
$ipAddress = (sizeof($ipAddressArray) > 0) ? $ipAddressArray[0] : '';
$_SERVER['REMOTE_ADDR'] = $ipAddress;

// 这里是启用会话的逻辑 
$session_started = false;
// 首先这里对应后台 Force Cookie use, 如果为True,那么先设置一个cookie,如果检测到有内容回发说明支持cookie。首先,第一次访问不会启用会话,第二,所有不支持cookie的都不启用会话,那么如果是爬行蜘蛛,则不会发送cookie,说明也起到了阻止机器人SESSION
if (SESSION_FORCE_COOKIE_USE == 'True') {
  	zen_setcookie('cookie_test', 'please_accept_for_session', time()+60*60*24*30, '/', (zen_not_null($current_domain) ? $current_domain : ''));

  	if (isset($_COOKIE['cookie_test'])) {
    	zen_session_start();
    	$session_started = true;
  	}
// 这里对应后台的 Prevent Spider Sessions,如果强制使用cookie,这里就没有意义(因为如果强制使用cookie,那么也就意味着阻止机器人会话了),当没有强制使用cookie来开启会话时,特别阻止机器人SESSION是有意义的。
} elseif (SESSION_BLOCK_SPIDERS == 'True') {
} else {
  	zen_session_start();
  	$session_started = true;
}
unset($spiders);
// 这个对应后台的 IP to Host Conversion Status  这个是根据IP反查主机地址 一般没有必要启用
if (!isset($_SESSION['customers_host_address'])) {
  if (SESSION_IP_TO_HOST_ADDRESS == 'true') {
    $_SESSION['customers_host_address']= @gethostbyaddr($_SERVER['REMOTE_ADDR']);
  } else {
    $_SESSION['customers_host_address'] = OFFICE_IP_TO_HOST_ADDRESS;
  }
}
// 这个对应后台的 Check SSL Session ID  如果请求类型是SSL,设置是否效验SSL_SESSION_ID,这个对应防止SSL挟持有一定作用
if ( ($request_type == 'SSL') && (SESSION_CHECK_SSL_SESSION_ID == 'True') && (ENABLE_SSL == 'true') && ($session_started == true) ) {
  	$ssl_session_id = $_SERVER['SSL_SESSION_ID'];
  	if (!$_SESSION['SSL_SESSION_ID']) {
    	$_SESSION['SSL_SESSION_ID'] = $ssl_session_id;
  	}
  	if ($_SESSION['SSL_SESSION_ID'] != $ssl_session_id) {
    	zen_session_destroy();
    	zen_redirect(zen_href_link(FILENAME_SSL_CHECK));
  	}
}
// 这个对应后台的 Check User Agent 效验用户代理是否一致,防止会话挟持,一般应该开启
if (SESSION_CHECK_USER_AGENT == 'True') {
}
// 这个对应后台的 Check IP Address 效验用户的访问IP是否以一致,一般不应该开启,现在的很多访问是同一个会话,但是来源IP可能不一样,因为用户可能从一个代理进来
if (SESSION_CHECK_IP_ADDRESS == 'True') {
}

另外一个对SESSION的封装主要在includes/functions/sessions.php文件:

  // 针对前后台设置会话有效时间,这个参数主要影用来设置保存在数据库中的会话过期时间
  if (IS_ADMIN_FLAG === true) {
    if (!$SESS_LIFE = (SESSION_TIMEOUT_ADMIN > 900 ? 900 : SESSION_TIMEOUT_ADMIN)) {
      $SESS_LIFE = (SESSION_TIMEOUT_ADMIN > 900 ? 900 : SESSION_TIMEOUT_ADMIN);
    }
  } else {
    if (!$SESS_LIFE = get_cfg_var('session.gc_maxlifetime')) {
      $SESS_LIFE = 1440;
    }
  }
  
  // 使用session_set_save_handler()函数修改会话内容保存的介质,zen-cart150以后,用户不能选择保存到文件,只能是保存到数据库
  session_set_save_handler('_sess_open', '_sess_close', '_sess_read', '_sess_write', '_sess_destroy', '_sess_gc');
  
  // 会话开始的封装,这里检查了会话ID是否是合法值,伪造的可能非法,这个检查非常有必要,另外设置一个会话安全码给securityToken,这样表单中提交的数据如果没有这个值,或者值对应不上,请求就会被终止,这个设置有效拒绝了来自第三方提交的表单数据。
  function zen_session_start() {
    @ini_set('session.gc_probability', 1);
    @ini_set('session.gc_divisor', 2);
    if (IS_ADMIN_FLAG === true) {
      @ini_set('session.gc_maxlifetime', (SESSION_TIMEOUT_ADMIN > 900 ? 900 : SESSION_TIMEOUT_ADMIN));
    }
    if (preg_replace('/[a-zA-Z0-9]/', '', session_id()) != '')
    {
      zen_session_id(md5(uniqid(rand(), true)));
    }
    $temp = session_start();
    if (!isset($_SESSION['securityToken'])) {
      $_SESSION['securityToken'] = md5(uniqid(rand(), true));
    }
    return $temp;
  }

  // 以下是对session_id()函数的封装,如果提过了SESSION ID(比如zen_session_start()函数会用到此情况),那么就要检查这个值是合法的
  function zen_session_id($sessid = '') {
    if (!empty($sessid)) {
      $tempSessid = $sessid;
  	  if (preg_replace('/[a-zA-Z0-9]/', '', $tempSessid) != '')
  	  {
  	    $sessid = md5(uniqid(rand(), true));
  	  }
      return session_id($sessid);
    } else {
      return session_id();
    }
  }

原创文章,转载务必保留出处。
http://blog.ifeeline.com/310.html