Laravel - 即使对于不同的用户

时间:2015-06-09 09:53:25

标签: php session authentication laravel-5

今天我在检查由Laravel 5创建的storage/framework/sessions文件夹中的会话文件时发现了令人不安的事情。

以下是发生的事情:

  1. 我以用户A
  2. 登录
  3. 我导航到一个在Session
  4. 中存储变量X的页面
  5. 我退出了,但没有关闭浏览器。
  6. storage/framework/sessions中的会话文件仍然存在,浏览器 饼干还活着。
  7. 我以用户B登录。
  8. storage/framework/sessions中的旧会话文件已被删除,并且存在新的会话文件。
  9. 我查看了新的会话文件 - 惊喜!变量X在登出后仍然存活,并且仍在那里,可供用户B访问!
  10. 这会导致安全问题,因为现在用户B可以访问用户A的数据。

    在通过Laravel源代码进行调试时,我发现在注销/登录过程中永远不会清除会话存储。仅在Illuminate\Auth\Guard::clearUserDataFromStorage()方法中删除了登录凭据,但所有会话商店属性仍然存在,稍后调用$kernel->terminate($request, $response);时,这会导致Illuminate\Session\Middleware\StartSession::terminate()调用{{1}盲目地将Store::save()保存到新会话中,忽略了它现在属于另一个用户的事实。

    从一方面来看,它似乎是合乎逻辑的--Laravel对我的数据没有任何假设,我是否希望它与认证一起过期。但是,如果将某些敏感数据附加到身份验证对象并与其一起过期,则将其记录在某处可能会很棒。

    这意味着我作为程序员负责在新的(或相同的)用户登录时完全清除当前会话中的所有敏感数据。

    注销时清除将不可靠,因为用户可能永远不会单击“注销”链接,而是等待会话“#34;到期”#34;这对于Laravel仍然无法清除会话。

    要记住的另一件事是:我不应该过早清除会话 - 必须存在AntiForgery令牌,否则登录表单将始终失败。

    我找到了一个论坛主题,也试图解决一些类似的问题:

    http://laravel.io/forum/04-27-2014-how-to-expire-session-data

    我对此感到困惑:

      

    我今天又做了一件事并意识到问题所在:Session :: flush()不会删除应用创建的会话数据,例如购物车详情

    如果这是真的,那么完全摆脱会话的唯一方法就是使用PHP原生$this->attributessession_unset(),但我不想这样做 - 我会如果可能的话,我更愿意找到更清洁的Laravel-ish解决方案。

    当身份验证过期或用户注销时,如何告诉Laravel我希望将旧会话数据与用户身份验证数据一起删除?

2 个答案:

答案 0 :(得分:5)

laravel docs它说你可以:

从会话中删除项目

Session::forget('key');

从会话中删除所有项目

Session::flush();

您可以导航到AuthenticatesAndRegistersUsers.php特征并重写

   /**
     * Log the user out of the application.
     *
     * @return \Illuminate\Http\Response
     */
    public function getLogout()
    {
        $this->auth->logout();

        return redirect(property_exists($this, 'redirectAfterLogout') ? $this->redirectAfterLogout : '/');
    }

   /**
     * Log the user out of the application.
     *
     * @return \Illuminate\Http\Response
     */
    public function getLogout()
    {
        Session::flush();

        $this->auth->logout();

        return redirect(property_exists($this, 'redirectAfterLogout') ? $this->redirectAfterLogout : '/');
    }

我不知道这是否真的有效,但试一试:)

<强>更新

According to this answer here on Stack Overflow,您可以将会话设置为在浏览器关闭时或XXX分钟后过期。与上述解决方案一起使用,应该解决问题吗?

在config / session.php

   /*
    |--------------------------------------------------------------------------
    | Session Lifetime
    |--------------------------------------------------------------------------
    |
    | Here you may specify the number of minutes that you wish the session
    | to be allowed to remain idle before it expires. If you want them
    | to immediately expire on the browser closing, set that option.
    |
    */

    'lifetime' => 120,

    'expire_on_close' => false

答案 1 :(得分:0)

我相信这是对这个问题/问题的正确答案:

在一个测试中发出多个请求时,在两个请求之间不会重置laravel应用程序的状态。身份验证管理器是laravel容器中的一个单例,它保留已解析身份验证防护的本地缓存。解析的身份验证防护会保留已身份验证用户的本地缓存。

因此,您对api /注销端点的第一个请求将解析auth管理器,该解析器将解析api保护器,该保护器存储对要撤销其令牌的已认证用户的引用。

现在,当您向/ api / user发出第二个请求时,已解决的auth管理器将从容器中拉出,已解决的api Guard从其本地缓存中拉出,而同一已解决的用户也将从容器中拉出守卫的本地缓存。这就是第二个请求通过身份验证而不是失败的原因。

在同一测试中测试具有多个请求的auth相关内容时,您需要在测试之间重置已解析的实例。另外,您不能只取消设置已解析的身份验证管理器实例,因为当再次解析它时,将不会定义扩展的护照驱动程序。

因此,我找到的最简单的方法是使用反射在已解析的身份验证管理器上取消设置保护的防护属性。您还需要在已解决的会话防护上调用注销方法。

来源:Method Illuminate\Auth\RequestGuard::logout does not exist Laravel Passport

要使用它,请将其添加到:

TestCase.php

protected function resetAuth(array $guards = null) : void
{
    $guards = $guards ?: array_keys(config('auth.guards'));

    foreach ($guards as $guard) {
        $guard = $this->app['auth']->guard($guard);

        if ($guard instanceof SessionGuard) {
            $guard->logout();
        }
    }

    $protectedProperty = new \ReflectionProperty($this->app['auth'], 'guards');
    $protectedProperty->setAccessible(true);
    $protectedProperty->setValue($this->app['auth'], []);
}

然后,像这样使用它:

LoginTest.php

class LoginTest extends TestCase
{
    use DatabaseTransactions, ThrottlesLogins;

    protected $auth_guard = 'web';

    /** @test */
    public function it_can_login()
    {
        $user = $this->user();

        $this->postJson(route('login'), ['email' => $user->email, 'password' => TestCase::AUTH_PASSWORD])
            ->assertStatus(200)
            ->assertJsonStructure([
                'user' => [
                    'id' ,
                    'status',
                    'name',
                    'email',
                    'email_verified_at',
                    'created_at',
                    'updated_at',
                    'photo_url',
                    'roles_list',
                    'roles',
                ],
            ]);

        $this->assertEquals(Auth::check(), true);
        $this->assertEquals(Auth::user()->email, $user->email);
        $this->assertAuthenticated($this->auth_guard);
        $this->assertAuthenticatedAs($user, $this->auth_guard);

        $this->resetAuth();
    }

    /** @test */
    public function it_can_logout()
    {
        $this->actingAs($this->user())
            ->postJson(route('logout'))
            ->assertStatus(204);

        $this->assertGuest($this->auth_guard);

        $this->resetAuth();
    }

    /** @test */
    public function it_should_get_two_cookies_upon_login_without_remember_me()
    {
        $user = $this->user();

        $response = $this->postJson(route('login'), [
            'email' => $user->email,
            'password' => TestCase::AUTH_PASSWORD,
        ]);

        $response->assertCookieNotExpired(Str::slug(config('app.name'), '_').'_session');
        $response->assertCookieNotExpired('XSRF-TOKEN');
        $this->assertEquals(config('session.http_only'), true);

        $this->resetAuth();
    }

    /** @test */
    public function it_should_get_three_cookies_upon_login_with_remember_me()
    {
        $user = $this->user();

        $response = $this->postJson(route('login'), [
            'email' => $user->email,
            'password' => TestCase::AUTH_PASSWORD,
            'remember' => true,
        ]);

        $response->assertCookieNotExpired(Str::slug(config('app.name'), '_').'_session');
        $response->assertCookieNotExpired('XSRF-TOKEN');
        $response->assertCookieNotExpired(Auth::getRecallerName());

        $this->resetAuth();
    }

    /** @test */
    public function it_should_throw_error_422_on_login_attempt_without_email()
    {
        $this->postJson(route('login'), ['email' => '', 'password' => TestCase::AUTH_PASSWORD])
            ->assertStatus(422)
            ->assertJsonStructure(['message', 'errors' => ['email']]);

        $this->assertGuest($this->auth_guard);

        $this->resetAuth();
    }

    /** @test */
    public function it_should_throw_error_422_on_login_attempt_without_password()
    {
        $this->postJson(route('login'), ['email' => $this->adminUser()->email, 'password' => ''])
            ->assertStatus(422)
            ->assertJsonStructure(['message', 'errors' => ['password']]);

        $this->assertGuest($this->auth_guard);

        $this->resetAuth();
    }

    /** @test */
    public function it_should_throw_error_422_on_login_attempt_with_empty_form()
    {
        $this->postJson(route('login'), ['email' => '', 'password' => ''])
            ->assertStatus(422)
            ->assertJsonStructure(['message', 'errors' => ['email', 'password']]);

        $this->assertGuest($this->auth_guard);

        $this->resetAuth();
    }

    /** @test */
    public function it_should_throw_error_401_as_guest_on_protected_routes()
    {
        $this->assertGuest($this->auth_guard);

        $this->getJson(route('me'))
            ->assertStatus(401)
            ->assertJson(['message' => 'Unauthenticated.']);
    }

    /** @test */
    public function it_should_throw_error_429_when_login_attempt_is_throttled()
    {
        $this->resetAuth();

        $throttledUser = factory(User::class, 1)->create()->first();

        foreach (range(0, 9) as $attempt) {
            $this->postJson(route('login'), ['email' => $throttledUser->email, 'password' => "{TestCase::AUTH_PASSWORD}_{$attempt}"]);
        }

        $this->postJson(route('login'), ['email' => $throttledUser->email, 'password' => TestCase::AUTH_PASSWORD . 'k'])
            ->assertStatus(429)
            ->assertJson(['message' => 'Too Many Attempts.']);

        $this->resetAuth();
    }

}

关于节气门的说明。我花了几天的时间弄清楚如何确保429行为。较早的单元测试将增加导致节流的“尝试”次数,因此您需要在节气门测试之前进行resetAuth,否则将在错误的时间触发节气门并拧紧测试。

鉴于上面的单元测试代码,我正在使用它:

Route::group(['middleware' => ['guest', 'throttle:10,5']], function () { /**/ });

您可以通过将这些数字中的任何一个(例如,将10,5更改为9,511,5)来观察其工作,并观察它如何影响油门单元测试。您还可以取消注释resetAuth方法的注释,并观察它如何固定测试。

对于单元测试中与auth相关的任何内容,resetAuth实用程序方法非常有用,必须具备。另外,必须具有AuthManager中的auth缓存知识,才能使观察到的行为有意义。