Laravel5.5源码详解 -- Session的启动分析
Laravel5.5源码详解 – Session的启动分析
Session的整个过程包括三个主要流程(laravel默认的sesssion名称都是laravel_session),
(1)启动session,
(2)操作session,对数据进行CRUD增删改查操作,
(3)关闭session。
Session启动之后的操作,和数据库的操作类似。这里不打算讲解。这里只关注启动过程,其一是因为session本身,这里相对比较难理解,其二是因为,CSRF等都是在session的启动过程中传入进去的,所以有必要了解。
首先看一下调用关系,session作为中间件由pipeline引入,我们从session的handle函数开始,这个函数在
\Illuminate\Session\Middleware\StartSession::class。
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()) {
$request->setLaravelSession(
$session = $this->startSession($request)
);
$this->collectGarbage($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->addCookieToResponse($response, $session);
}
return $response;
}
其中Handle调用 startSession
再调用 getSession & setRequestOnHandler
。我们一步一步来,慢慢剥开这些外衣,
protected function startSession(Request $request)
{
return tap($this->getSession($request), function ($session) use ($request) {
//下面这句没细查,先跳过。我调试了下,没发现有干任何有意义的事情,可能是采用redis等时才需要,
//这里配置的是file。与其相关的接口是SessionHandlerInterface,这个一般是php对session接口设计的,
//如memcached, redis等。
$session->setRequestOnHandler($request);
//关键是后面这句
$session->start();
});
}
startSession()包括两步,首先是获取session的实例,也就是\Illuminate\Session\Store,主要步骤是 session= this->manager->driver(),下面随后讲解,第二步,就是通过该实例从存储介质中读取所需要的数据,相关代码在$session->start()中体现。
第一步的源码:获取session实例\Illuminate\Session\Store
在SessionStart.php里,
public function getSession(Request $request)
{
return tap($this->manager->driver(), function ($session) use ($request) {
$session->setId($request->cookies->get($session->getName()));
});
}
这个$this->manager->driver()是这么个东东?
Store {#325 ▼
id: "4qbYEQItvRNIJKSfAeRO0rGT1S4JGk3bAuedRFEJ"
name: "laravel_session"
attributes: []
handler: FileSessionHandler {#326 ▼
#files: Filesystem {#101}
#path: "D:\wamp64\www\laravel\larablog\storage\framework/sessions"
#minutes: "120"
}
started: false
}
说明白点,$this->manager->driver()就相当于StartSession->SessionManager->Store。也就是session实例本尊。这个manager,就是SessionManger类。
那么laravel的sessionManager是什么时候配置store的呢?
本质上,是调用了父类Illuminate\Support\Manager里面的driver()函数,该函数又调用Illuminate\Session\SessionManager中的getDefaultDriver()函数。我们先看getDefaultDriver()函数
public function getDefaultDriver()
{
return $this->app[‘config’][‘session.driver’];
}
这个app[‘config’]的来源,后面还会详细讲解,这理先了解结果。总之,就是把/bootstrap/cache/config.php 里面的配置项session.driver拉出来。比如我的配置项是这样的,
'session' =>
array (
'driver' => 'file',
'lifetime' => '120',
'expire_on_close' => false,
'encrypt' => false,
'files' => 'D:\\wamp64\\www\\laravel\\larablog\\storage\\framework/sessions',
…此处省略若干行
)
再看driver()函数。整体上,driver()函数是下面这个样子,其中的createDriver后面还会讲到,这个又和buildStore有关。
public function driver($driver = null)
{
//得到/bootstrap/cache/config.php 里面的配置项
$driver = $driver ?: $this->getDefaultDriver();
//如果这个store不存在的话,就创建一个,store实例就是在这创建的。
if (! isset($this->drivers[$driver])) {
$this->drivers[$driver] = $this->createDriver($driver);
}
return $this->drivers[$driver];
}
最后 this−>manager−>driver()返回来的就是这么个 this->drivers[“file”],也就是下面的store,
Store {#326 ▼
id: "cp0yC4HOkgwAq2h9FfGeLF98RM3IgscXkENFRFnp"
name: "laravel_session"
attributes: []
handler: FileSessionHandler {#327 ▼
#files: Filesystem {#101}
#path: "D:\wamp64\www\laravel\larablog\storage\framework/sessions"
#minutes: "120"
}
started: false
}
说明一下,这个id没有什么实质意义,每次刷新都会不同。
那么我们明白,startSession 首先运行的就是getSession(),其中得到的是store,也就是$session。
再注意getSession()中这一小行,$session->setId($request->cookies->get($session->getName()));
$request->cookies是在request创建时就传递进去的,是一个数组,如下,
cookies: ParameterBag {#21 ▼
#parameters: array:2 [▼
"XSRF-TOKEN" => "eyJpdiI6IllOSStvZlMwNEk0T084eWJVWFZ5VVE9PSI▶"
"laravel_session" => "eyJpdiI6IlRNSCtGcmlkeEEybGc5TzUzdXRyR ▶"
]
}
这里要说一句,就是读者不必去关注XSRF-TOKEN的具体值,因为调试时,每次都会不一样,在实际运行时,只有一个。
接上面,$session->getName()
就是那个名称”laravel_session”,经过$session->setId()处理之后,会得到这么一个store,
Store {#325 ▼
id: "0z8Sd3lsnJYIkTBILQeamqh80floYcF0InWwdgm2"
name: "laravel_session"
attributes: []
handler: FileSessionHandler {#326 ▼
#files: Filesystem {#101}
#path: "D:\wamp64\www\laravel\larablog\storage\framework/sessions"
#minutes: "120"
}
started: false
}
还是前面提到的那个store,也是getSession的返回值。
第二步,读取session数据
startSession在得到$session(也就是store)之后,接下来会启动下面这个start,现在来看Session->start()
public function start()
{
//最重要的一条语句,实质是读取保存的session数据
$this->loadSession();
//如果没有CSRF_TOKEN,就创建一个
if (! $this->has('_token')) {
$this->regenerateToken();
}
//session启动完毕(就是取到了需要的数据而已),返回
return $this->started = true;
}
protected function loadSession()
{
$this->attributes = array_merge($this->attributes, $this->readFromHandler());
}
protected function readFromHandler()
{
//read读到session中的数据
if ($data = $this->handler->read($this->getId())) {
//反序列化 -- 从已存储的session数据中创建 PHP 的值。
$data = @unserialize($this->prepareForUnserialize($data));
//确定读到的数据是非空数组。
if ($data !== false && ! is_null($data) && is_array($data)) {
return $data;
}
}
return [];
}
在上面的函数中,readFromHandler里用到的这个handler是指向下面这个我们用来存储session数据的具体位置,
FileSessionHandler {#326 ▼
files: Filesystem {#101}
path: "D:\wamp64\www\laravel\larablog\storage\framework/sessions"
minutes: "120"
}
必须特别说明的是,这里我用的是laravel默认的file,也就是文件来存储session。所以,handler也就是FileSessionHandler。如果你的配置不一样,那么handler就会不一样。
继续深入readFromhandler之前,还要了解以下两点:store的创建和参数传递,及FileSessionHandler。
Store是什么时候被创建的?如何传递参数。
现在回过头来看前面提到的createDriver了,前面已经提到过,该函数被Illuminate\Support\Manager里面的driver()函数调用。其原型是这样子的,
protected function createDriver($driver)
{
// We'll check to see if a creator method exists for the given driver. If not we
// will check for a custom driver creator, which allows developers to create
// drivers using their own customized driver creator Closure to create it.
if (isset($this->customCreators[$driver])) {
return $this->callCustomCreator($driver);
} else {
$method = 'create'.Str::studly($driver).'Driver';
if (method_exists($this, $method)) {
return $this->$method();
}
}
throw new InvalidArgumentException("Driver [$driver] not supported.");
}
这里面的$method,如果你打印出来的话,正是”createFileDriver”。所以,最终创建store的命令,也就是下面要提到的createFileDriver。这也是因为我们使用的是file作为store配置的结果,所以看这个
protected function createFileDriver()
{
return $this->createNativeDriver();
}
protected function createNativeDriver()
{
$lifetime = $this->app['config']['session.lifetime'];
return $this->buildSession(new FileSessionHandler(
$this->app['files'], $this->app['config']['session.files'], $lifetime
));
}
protected function buildSession($handler)
{
if ($this->app['config']['session.encrypt']) {
return $this->buildEncryptedSession($handler);
} else {
//默认的情况下是不加密的session,所以执行下面这句,
return new Store($this->app['config']['session.cookie'], $handler);
}
}
最后这里createFileDriver调用了createNativeDriver,其中又用new createNativeDriver创建了这个handler作为参数,传递给buildSession ,buildSession再用这个handler创建了store。最重要的,是要了解new Store($this->app['config']['session.cookie'], $handler)
这一句,Store也正是在这里被创建的。
了解FileSessionHandler
前面已经特别强调,那个反复提到的handler是FileSessionHandler,他实现了SessionHandlerInterface这个PHP接口。
回想起前面提到的readFromHandler这个函数,其中最关键的就是获取session数据这一句:$data = $this->handler->read($this->getId())
,这个read,也正是调用了FileSessionHandler里的read函数
public function read($sessionId)
{
if ($this->files->exists($path = $this->path.'/'.$sessionId)) {
if (filemtime($path) >= Carbon::now()->subMinutes($this->minutes)->getTimestamp()) {
return $this->files->get($path, true);
}
}
return '';
}
把那个$path打印出来会是下面这个样子,这也是数据存储的文件名(是不是觉得名字好怪?)。对照看一下,
"D:\wamp64\www\laravel\larablog\storage\framework/sessions/0z8Sd3lsnJYIkTBILQeamqh80floYcF0InWwdgm2"
该read函数最后返回的$this->files->get($path, true)
正是文件里的内容,其中包括那个大名鼎鼎的CSRF-TOKEN。
a:4:{
s:6:"_token";s:40:"zalGw4lffAeW6alsQKFBxb54QE9o3hIpEb0kK3Sd";
s:9:"_previous";
a:1:{s:3:"url";s:33:"http://localhost:8000/admin/login";}
s:6:"_flash";
a:2:{ s:3:"old";a:0:{}s:3:"new";a:0:{} }
s:22:"PHPDEBUGBAR_STACK_DATA";
a:0:{}
}
这个数据再经过前面提到的readFromhandler的反序列化unserialize操作,会是下面这个样子
array:4 [▼
"_token" => "zalGw4lffAeW6alsQKFBxb54QE9o3hIpEb0kK3Sd"
"_previous" => array:1 [▼
"url" => "http://localhost:8000/admin/login"
]
"_flash" => array:2 [▼
"old" => []
"new" => []
]
"PHPDEBUGBAR_STACK_DATA" => []
]
到这里,极为值得一提的是那个_token,你是不是在程序调试CSRF_TOKEN时经常碰到TokenMismatchException的问题?到这里查一下,说不定有意想不以的收获!
另外,关于这个CSRF-TOKEN,我们看到有两个地方,一个是在这里直接引入,如果这里没有,就会在Session->start()中用regenerateToken()创建。
至此,startSession完全运行完毕。
但还有些疑问,比如session是什么时候配置的?
在前面讲到的handle 函数中,在startSession之前,有这么一句配置函数
if ($this->sessionConfigured()) 。。。
这个sessionConfigured是这样的,
protected function sessionConfigured()
{
return ! is_null($this->manager->getSessionConfig()['driver'] ?? null);
}
该函数再调用sessionManager的getSessionConfig()来寻找配置,
直接dump($this)把sessionManager打印出来,看看这到底是个什么东西?(代码很长,所有有省略)
SessionManager {#193 ▼
app: Application {#2 ▶}
customCreators: []
drivers: array:1 [▼
file" => Store {#490 ▼
#id: "8bfH3MIt3iY5BiNUae14sCRd6QT1ScY2QwHdbBSE"
#name: "laravel_session"
#attributes: []
#handler: FileSessionHandler {#586 ▼
#files: Filesystem {#101}
#path: "D:\wamp64\www\laravel\larablog\storage\framework/sessions"
#minutes: "120"
}
#started: false
}
]
}
在sessionManger中加入调试代码,
public function getSessionConfig()
{
dump($this);
dd();
return $this->app['config']['session'];
}
得到下面的结果,
$this就是SessionManager,
SessionManager {#193 ▼
app: Application {#2 ▼
#basePath: "D:\wamp64\www\laravel\larablog"
…
}
customCreators: []
drivers: []
}
$this->app[‘config’]是下面这个东西,
Repository {#24 ▼
items: array:14 [▼
"app" => array:13 [▶]
"auth" => array:4 [▶]
"broadcasting" => array:2 [▶]
"cache" => array:3 [▶]
"database" => array:4 [▶]
"debugbar" => array:13 [▶]
"filesystems" => array:3 [▶]
"mail" => array:9 [▶]
"queue" => array:3 [▶]
"scout" => array:5 [▶]
"services" => array:4 [▶]
"session" => array:15 [▶]
"view" => array:2 [▶]
"trustedproxy" => array:2 [▶]
]
}
要明白那个$this->app[‘config’]是如何得到Repository的,看下面:config 配置文件的加载。
插曲:config 配置文件的加载
config 配置文件由类 \Illuminate\Foundation\Bootstrap\LoadConfiguration::class 完成:
public function bootstrap(Application $app)
{
$items = [];
if (file_exists($cached = $app->getCachedConfigPath())) {
//注意,这个$cached返回的是路径,
//"D:\wamp64\www\laravel\larablog\bootstrap/cache/config.php"
$items = require $cached;
//items就相当于/bootstrap/cache/config.php里面的所有配置项
$loadedFromCache = true;
}
//下面,new Repository($items)创建一个仓库,并把所有配置项作为参数传递进去,
//然后绑定到$app->instances数组中的config上,说明config已经实例化。
$app->instance('config', $config = new Repository($items));
//此时,如果打印出$app,就会得到后面的那些内容,可以明显看到,
//instances数组中添加了’config’ (已经刻意展开了该项)
if (! isset($loadedFromCache)) {
$this->loadConfigurationFiles($app, $config);
}
$app->detectEnvironment(function () use ($config) {
return $config->get('app.env', 'production');
});
date_default_timezone_set($config->get('app.timezone', 'UTC'));
mb_internal_encoding('UTF-8');
}
dd(#app)得到的结果,
Application {#2 ▼
basePath: "D:\wamp64\www\laravel\larablog"
…
instances: array:17 [▼
…
"config" => Repository {#24 ▼
#items: array:14 [▼
"app" => array:13 [▶]
"auth" => array:4 [▶]
"broadcasting" => array:2 [▶]
"cache" => array:3 [▶]
"database" => array:4 [▶]
"debugbar" => array:13 [▶]
"filesystems" => array:3 [▶]
"mail" => array:9 [▶]
"queue" => array:3 [▶]
"scout" => array:5 [▶]
"services" => array:4 [▶]
"session" => array:15 [▶]
"view" => array:2 [▶]
"trustedproxy" => array:2 [▶]
]
…
}
这个配置,也就是前面的getDefaultDriver()中拉出来用的配置。
当然,有兴趣的朋友可以继续研究loadConfigurationFiles。
至此,我们已经 全面了解这个session是如何启动的了。