Laravel Session 遇到的坑

这两天遇到了一个很奇怪的问题,更新 sessionsession 的值不变。经过一番追查,终于找到问题,并搞明白了原理。写这篇博客记录下。

框架版本#

Laravel 5.4

问题#

先来描述下问题,我在我们项目基础的 Middleware 中,加入 session 操作,存入了一个值,再在 Controller 中取出使用,大致代码如下:

// Middleware
public function handle($request, Closure $next)
{
    $id = Redis::get('id');
    session(['id' => $id]);
    return $next($request);
}

// Controller
public function index()
{
    $id = session('id');
    return ['id' => $id];
}

假设 reids 中的 id 是 1,这一次访问 index 这个 action,返回的是 1,当你将 redisid 的值改成 2 时,在访问,发现返回的还是 1,而且之后的访问也都是 1。这里说明一下 session 使用的是 redis

解决问题#

看到这样神奇的结果,百思不得其解。于是打开 Xdebug,开始调试。经过多次调试,发现在执行完
\Illuminate\Session\Middleware\StartSession 这个 Middleware 后,session 里面的值就变回 1 了,在之前都是 2。然后想到会不会我们的 MiddlewareStartSession 之前执行造成的,将我们的 Middleware 移到 StartSession 之后,发现果然可以了,app/Http/Kernel.php 中的代码如下:

protected $middlewareGroups = [
    'web' => [
        \Illuminate\Cookie\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \App\Http\Middleware\OurMiddleware::class,
    ]
];

其中的 OurMiddleware 是我们自己写的 Middleware,之前是放在最上面的,$next($request) 之前的代码的执行顺序是从上到下的,如果 OurMiddleware 中有些内容是必须在最开始的,可以考虑分成两个 Middleware

理解原理#

虽然解决了问题,但还是不知道其原理究竟是怎样的,带着这样的疑问我继续查看源码,最终找到了相应的内容。

  • session 不是实时落地的,也就是说当你调用 session(['id' => $id]) 时,id 并没有被真正存入 redis 中,而是缓存在 \Illuminate\Session\Store 单例的 attributes 属性中,可以查看其 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);
        }
    }
  • \Illuminate\Session\Middleware\StartSession 在执行时,回自动加载 redis 中已经实例化的数据,并覆盖 \Illuminate\Session\Store 单例中的 attributes 属性,所以这就导致我们一直取到的都是 redis 中的 session 数据。加载覆盖的代码如下:

    protected function loadSession()
    {
        $this->attributes = array_merge($this->attributes, $this->readFromHandler());
    }
    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 [];
    }

其中的 readFromHandler 方法就是获取 redis 中的 session 数据。

后记#

其实这不是 Laravel session 的坑,是我自己踩坑,原谅我是个标题党:smile:

本作品采用《CC 协议》,转载必须注明作者和本文链接
Talk is cheap. Show me the code.
本帖由 Summer 于 5年前 加精
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 4

@edwin404 感谢支持~

8年前 评论

老铁,我在中间件里清除所有 session,清除后打印,是 null,但是执行下来,发现并没又清掉,知道中间件里要怎么写么。。。

6年前 评论

@gaoxiang 肯定是之后某个地方覆盖掉了,这需要排查一下

6年前 评论