在网页开发中, session
具有重要的作用,它可以在多个请求中存储用户的信息,用于识别用户的身份信息。laravel
为用户提供了可读性强的 API 处理各种自带的 Session 后台驱动程序。支持诸如比较热门的 Memcached、Redis 和开箱即用的数据库等常见的后台驱动程序。本文将会在本篇文章中讲述最常见的由 File
与 redis
驱动的 session
源码。
与其他功能一样,session
由自己的服务提供者在 container
内进行注册:
class SessionServiceProvider extends ServiceProvider
{
public function register()
{
$this->registerSessionManager();
$this->registerSessionDriver();
$this->app->singleton(StartSession::class);
}
protected function registerSessionManager()
{
$this->app->singleton('session', function ($app) {
return new SessionManager($app);
});
}
protected function registerSessionDriver()
{
$this->app->singleton('session.store', function ($app) {
return $app->make('session')->driver();
});
}
}
可以看到 SessionManager
是整个 session
服务的接口类,一切对 session
的操作都是由这个类实现。session.store
是 session
服务的存储驱动。
session
服务是以中间件的形式启动的,其中间件是 Illuminate\Session\Middleware\StartSession
:
public function handle($request, Closure $next)
{
$this->sessionHandled = true;
if ($this->sessionConfigured()) {
$request->setLaravelSession(
$session = $this->startSession($request)
);
$this->collectGarbage($session);
}
$response = $next($request);
if ($this->sessionConfigured()) {
$this->storeCurrentUrl($request, $session);
$this->addCookieToResponse($response, $session);
}
return $response;
}
public function terminate($request, $response)
{
if ($this->sessionHandled && $this->sessionConfigured() && ! $this->usingCookieSessions()) {
$this->manager->driver()->save();
}
}
session
服务的中间件在 http
会话前与会话后都有处理。
在会话前,
laravel
试图从 cookies
中获取 sessionId
;sessionId
读取服务器中的 session
数据;session
对象存入 request
中;session
垃圾回收在会话后,
url
作为 session
的 PreviousUrl
session
存入浏览器 cookies
中session
数据到存储器驱动startSession
函数进行了 session
的启动工作:
public function __construct(SessionManager $manager)
{
$this->manager = $manager;
}
protected function startSession(Request $request)
{
return tap($this->getSession($request), function ($session) use ($request) {
$session->setRequestOnHandler($request);
$session->start();
});
}
public function getSession(Request $request)
{
return tap($this->manager->driver(), function ($session) use ($request) {
$session->setId($request->cookies->get($session->getName()));
});
}
代码很简洁,session
服务启动的逻辑被包含在了 sessionManager
中,sessionManager
是 session
服务的门面类,负责 session
服务的驱动加载与数据操作。
首先我们先看看 SessionManager
:
namespace Illuminate\Session;
use Illuminate\Support\Manager;
class SessionManager extends Manager
{
}
SessionManager
继承 Manager
类:
namespace Illuminate\Support;
abstract class Manager
{
public function driver($driver = null)
{
$driver = $driver ?: $this->getDefaultDriver();
if (! isset($this->drivers[$driver])) {
$this->drivers[$driver] = $this->createDriver($driver);
}
return $this->drivers[$driver];
}
}
当我们调用 driver
函数的时候,程序就开始为 session
服务加载驱动,例如对数据库或者 redis
驱动,进行 连接
操作。
public function getDefaultDriver()
{
return $this->app['config']['session.driver'];
}
protected function createDriver($driver)
{
$method = 'create'.Str::studly($driver).'Driver';
if (isset($this->customCreators[$driver])) {
return $this->callCustomCreator($driver);
} elseif (method_exists($this, $method)) {
return $this->$method();
}
throw new InvalidArgumentException("Driver [$driver] not supported.");
}
FileSessionHandler
这个类就是驱动,它继承 SessionHandlerInterface
基类,任何对 session
的读取、添加、删除、更新等等操作最后都要通过这个驱动类进行持久化。
file
驱动:file
驱动的核心是 Filesystem
,该类是 Ioc 容器创建的:
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
));
}
namespace Illuminate\Session;
class FileSessionHandler implements SessionHandlerInterface
{
public function __construct(Filesystem $files, $path, $minutes)
{
$this->path = $path;
$this->files = $files;
$this->minutes = $minutes;
}
}
redis
驱动并不是直接创建 redis
,而是利用了 laravel
的缓存 cache
系统创建 redis
驱动,然后对 redis
驱动进行连接操作:
protected function createRedisDriver()
{
$handler = $this->createCacheHandler('redis');
$handler->getCache()->getStore()->setConnection(
$this->app['config']['session.connection']
);
return $this->buildSession($handler);
}
protected function createCacheHandler($driver)
{
$store = $this->app['config']->get('session.store') ?: $driver;
return new CacheBasedSessionHandler(
clone $this->app['cache']->store($store),
$this->app['config']['session.lifetime']
);
}
class CacheBasedSessionHandler implements SessionHandlerInterface
{
public function __construct(CacheContract $cache, $minutes)
{
$this->cache = $cache;
$this->minutes = $minutes;
}
}
buildSession
函数将会返回 Store
类,这个 Store
类实际上 session
服务数据操作的实质类,任何对 session
数据的操作实际上调用的都是 Store
类:
protected function buildSession($handler)
{
if ($this->app['config']['session.encrypt']) {
return $this->buildEncryptedSession($handler);
} else {
return new Store($this->app['config']['session.cookie'], $handler);
}
}
protected function buildEncryptedSession($handler)
{
return new EncryptedStore(
$this->app['config']['session.cookie'], $handler, $this->app['encrypter']
);
}
public function __call($method, $parameters)
{
return $this->driver()->$method(...$parameters);
}
如果需要对 session
进行加密,那么就会创建一个 EncryptedStore
类,该类继承 Store
类。
session
驱动建立之后,就要进行 sessionId
的设置,如果 cookie
中存在 sessionId
,我们就会从中获取,否则我们就需要重新生成新的 sessionId
public function setId($id)
{
$this->id = $this->isValidId($id) ? $id : $this->generateSessionId();
}
public function isValidId($id)
{
return is_string($id) && ctype_alnum($id) && strlen($id) === 40;
}
protected function generateSessionId()
{
return Str::random(40);
}
一切准备就绪后,我们就要启动 session
,如果当前请求存在未过期 session
,那么就要利用 session
驱动将数据读取出来:
public function start()
{
$this->loadSession();
if (! $this->has('_token')) {
$this->regenerateToken();
}
return $this->started = true;
}
protected function loadSession()
{
$this->attributes = array_merge($this->attributes, $this->readFromHandler());
}
readFromHandler
函数就是读取 session
的过程:
protected function readFromHandler()
{
if ($data = $this->handler->read($this->getId())) {
$data = @unserialize($this->prepareForUnserialize($data));
if ($data !== false && ! is_null($data) && is_array($data)) {
return $data;
}
}
return [];
}
对于未加密的 session
来说,prepareForUnserialize
直接返回了数据:
protected function prepareForUnserialize($data)
{
return $data;
}
protected function prepareForUnserialize($data)
{
try {
return $this->encrypter->decrypt($data);
} catch (DecryptException $e) {
return serialize([]);
}
}
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 '';
}
public function read($sessionId)
{
return $this->cache->get($sessionId, '');
}
session
的垃圾回收用于随机性地删除旧 session
数据。由于某些驱动,例如 FileSessionHandler
, 程序不会定期删除那些已经过时的 session
文件,那么 session
文件一定会越来越多,所以我们就需要一种垃圾回收机制:
protected function collectGarbage(Session $session)
{
$config = $this->manager->getSessionConfig();
if ($this->configHitsLottery($config)) {
$session->getHandler()->gc($this->getSessionLifetimeInSeconds());
}
}
protected function configHitsLottery(array $config)
{
return random_int(1, $config['lottery'][1]) <= $config['lottery'][0];
}
configHitsLottery
函数就是判断当前是否被随机要进行垃圾回收任务。这种随机性概率由 lottery
来设置。
FileSessionHandler
的垃圾回收:
public function gc($lifetime)
{
$files = Finder::create()
->in($this->path)
->files()
->ignoreDotFiles(true)
->date('<= now - '.$lifetime.' seconds');
foreach ($files as $file) {
$this->files->delete($file->getRealPath());
}
}
很多时候我们都需要从 session
中获取前一页的地址,例如用户授权失败就会返回上一页等等情景。
protected function storeCurrentUrl(Request $request, $session)
{
if ($request->method() === 'GET' && $request->route() && ! $request->ajax()) {
$session->setPreviousUrl($request->fullUrl());
}
}
public function setPreviousUrl($url)
{
$this->put('_previous.url', $url);
}
当请求结束时,会调用中间件的 terminate
函数,这里程序会将新的 session
数据持久化到各个驱动器中:
public function terminate($request, $response)
{
if ($this->sessionHandled && $this->sessionConfigured() && ! $this->usingCookieSessions()) {
$this->manager->driver()->save();
}
}
session
的保存:
public function save()
{
$this->ageFlashData();
$this->handler->write($this->getId(), $this->prepareForStorage(
serialize($this->attributes)
));
$this->started = false;
}
session
的保存会删除需要 flash
的闪存数据,也就是只想用于下一次请求的数据:
public function ageFlashData()
{
$this->forget($this->get('_flash.old', []));
$this->put('_flash.old', $this->get('_flash.new', []));
$this->put('_flash.new', []);
}
对于不加密的数据,保存前的 prepareForStorage
不会对数据进行任何操作:
protected function prepareForStorage($data)
{
return $data;
}
对于加密的数据,则需要事先加密:
protected function prepareForStorage($data)
{
return $this->encrypter->encrypt($data);
}
当我们想要获取 session
中的数据时,我们经常使用 get
方法
public function show(Request $request, $id)
{
$value = $request->session()->get('key');
//
}
get
方法首先会调用 sessionManager
的魔术方法:
public function __call($method, $parameters)
{
return $this->driver()->$method(...$parameters);
}
driver
函数会返回 Store
对象,调用 get
方法
public function get($key, $default = null)
{
return Arr::get($this->attributes, $key, $default);
}
我们从上一节知道,在 startSession
中间件启动后,session
数据已经加载到了 store
对象中,因此获取数据很简单:
public function get($key, $default = null)
{
return Arr::get($this->attributes, $key, $default);
}
all
函数可以取出所有的 session
数据
public function all()
{
return $this->attributes;
}
要确定 Session 中是否存在某个值,可以使用 has
方法。如果该值存在且不为 null
,那么 has
方法会返回 true
:
public function has($key)
{
return ! collect(is_array($key) ? $key : func_get_args())->contains(function ($key) {
return is_null($this->get($key));
});
}
要确定 Session
中是否存在某个值,即使其值为 null
,也可以使用 exists
方法。如果值存在,则 exists
方法返回 true
public function exists($key)
{
return ! collect(is_array($key) ? $key : func_get_args())->contains(function ($key) {
return ! Arr::exists($this->attributes, $key);
});
}
要存储数据到 Session
,可以使用 put
方法
public function put($key, $value = null)
{
if (! is_array($key)) {
$key = [$key => $value];
}
foreach ($key as $arrayKey => $arrayValue) {
Arr::set($this->attributes, $arrayKey, $arrayValue);
}
}
push
方法可以将一个新的值添加到 Session
数组内。
public function push($key, $value)
{
$array = $this->get($key, []);
$array[] = $value;
$this->put($key, $array);
}
remember
方法用于有即取,无即存的情况:
public function remember($key, Closure $callback)
{
if (! is_null($value = $this->get($key))) {
return $value;
}
return tap($callback(), function ($value) use ($key) {
$this->put($key, $value);
});
}
increment
方法用于增加某 session
数据的值:
public function increment($key, $amount = 1)
{
$this->put($key, $value = $this->get($key, 0) + $amount);
return $value;
}
public function decrement($key, $amount = 1)
{
return $this->increment($key, $amount * -1);
}
pull
方法可以只用一条语句就从 Session
检索并且删除一个项目:
public function pull($key, $default = null)
{
return Arr::pull($this->attributes, $key, $default);
}
有时候你仅想在下一个请求之前在 Session
中存入数据,你可以使用 flash
方法。使用这个方法保存在 session
中的数据,只会保留到下个 HTTP
请求到来之前,然后就会被删除。闪存数据主要用于短期的状态消息
public function flash($key, $value)
{
$this->put($key, $value);
$this->push('_flash.new', $key);
$this->removeFromOldFlashData([$key]);
}
protected function removeFromOldFlashData(array $keys)
{
$this->put('_flash.old', array_diff($this->get('_flash.old', []), $keys));
}
闪存数据的实现很简单,session
中会维护两个数组:_flash.new
、_flash.old
,每次 session
结束前,都会删除 _flash.old
中的存储的 key
对应存储在 session
的 value
。
now
方法用于存储只有本次请求采用的数据
public function now($key, $value)
{
$this->put($key, $value);
$this->push('_flash.old', $key);
}
如果需要保留闪存数据给更多请求,可以使用 reflash 方法,这将会将所有的闪存数据保留给其他请求。
public function reflash()
{
$this->mergeNewFlashes($this->get('_flash.old', []));
$this->put('_flash.old', []);
}
这样,_flash.old
中的数据就会被合并到 _flash.new
中。
只想保留特定的闪存数据给更多请求,则可以使用 keep 方法:
public function keep($keys = null)
{
$this->mergeNewFlashes($keys = is_array($keys) ? $keys : func_get_args());
$this->removeFromOldFlashData($keys);
}
forget 方法可以从 Session 内删除一条数据。
public function forget($keys)
{
Arr::forget($this->attributes, $keys);
}
如果你想删除 Session 内所有数据,可以使用 flush 方法:
public function flush()
{
$this->attributes = [];
}
重新生成 Session ID
,通常是为了防止恶意用户利用 session fixation
对应用进行攻击。如果使用了内置函数 LoginController
,Laravel 会自动重新生成身份验证中 Session ID
。否则,你需要手动使用 regenerate
方法重新生成 Session ID
。
public function regenerate($destroy = false)
{
return $this->migrate($destroy);
}
public function migrate($destroy = false)
{
if ($destroy) {
$this->handler->destroy($this->getId());
}
$this->setExists(false);
$this->setId($this->generateSessionId());
return true;
}