SymfonyのRememberMeについて調べてみた

SymfonyのRememberMeについて調べてみた

March 26, 2020,
tags: symfony symfony4 php


このエントリーをはてなブックマークに追加

SymfonyのRememberMeとはなんぞや?今まで雰囲気で使ってたので調べてみた。

  1. RembemerMeとは?
  2. どのように実現しているのか?

RembemerMeとは?

公式ドキュメントによると以下のように記載されている。

ユーザーが認証されると、通常、ユーザーの資格情報がセッションに保存されます。つまり、セッションが終了すると、ユーザーはログアウトされ、次回アプリケーションにアクセスするときに再度ログイン詳細を提供する必要があります。remember_meファイアウォールオプション付きのCookieを使用して、セッションが継続する期間よりも長い間ログインしたままにすることをユーザーが選択できるようにすることができます。

https://symfony.com/doc/current/security/remember_me.html

つまり、セッションが切れてもログイン状態を継続することが出来る。

設定方法について

基本的には security.yaml に設定を記述していく

        main:
            remember_me:
                secret:   '%kernel.secret%'
                lifetime: 31536000

ベースはこんな感じ。

パラーメータの種類ついて

  1. secret(必須)
  2. name
  3. lifetime
  4. path
  5. domain
  6. secure
  7. httponly
  8. samesite
  9. remember_me_parameter
  10. always_remember_me
  11. token_provider

secret

Coookieコンテンツを暗号化するのに使う。 TokenBasedRememberMeServices::generateCookieHash()で利用されている

name

ユーザーのログインを維持するために使用されるCookieの名前。
デフォルトだと REMEMBERME

lifetime

ユーザーがログインし続ける秒数。デフォルトでは、1年間ログイン。
(cookieの有効期限)

path

Cookieパスの指定用。
デフォルトは /

domain

Cookieのドメイン設定。

secure

セキュアCookieかどうかの設定。
デフォルトは false (ほぼHTTPSな世界だし、trueにするのが良さそう)

httponly

cookieのhttponlyの設定。
デフォルトは false (ほぼHTTPSな世界だし、trueにするのが良さそう)

remember_me_parameter

RememberMeを有効にするかどうかの、フラグ用のフォームのinput名。
デフォルトは _remember_me

always_remember_me

強制的にRememberMeを有効にする。
つまりremember_me_parameterは無視される。

token_provider

トークンを保存するためのプロバイダを指定することが出来る。
デフォルトではCookieに保存されるが、DBに保存することもできる DoctrineTokenProvider

どのように仕組になっているのか?

RememberMe用の情報の保存について

基本的にはログインとは別にセッションを発行して、そこで管理していることになる。
基本的には認証後にRememberMeに関する情報をcookieに保存する形になっている。
GuardAuthenticationListener::triggerRememberMe() あたりを読んでみるとよく分かる。

token_providerを設定してないとTokenBasedRememberMeServices::onLoginSuccess()が呼び出されて、そこでユーザー名UserInterface::getUsername() とパスワード UserInterface::getPassword() の情報を暗号化してcookieに保存される仕組みになっている。

RememberMeでログインする処理について

RememberMeListener::authenticate() あたりで処理書かれている。
ログイン認証が続いているようなら、RememberMe処理をしないで、そうでなければRememberMeの情報をもとにログイン処理を行うという流れ。

デフォルトでは最終的には TokenBasedRememberMeServices::processAutoLoginCookie() が実行されて、security.yamlで設定しているUserProvider::loadUserByUsername() でユーザー情報が取得される。

TokenBasedRememberMeServicesを差し替えたい

今回は様々な事情でTokenBasedRememberMeServicesを差し替えたい。

どのようにDIに登録されているか調べる

Xdebugでコンストラクタにブレークポイント貼ったら security.authentication.rememberme.services.simplehash.main という名前でDIに登録されている。
ちなみに RememberMeFactory 見てみるとわかるが、 実際は ChildDefinitionとしてsecurity.authentication.rememberme.services.simplehashと登録しているなら全体的に差し替えるならこっちの名前で差し替えるのはありかも。

実際に差し替える

ここまで来ればあとはラップしたClassと、差し替えるようのCompilerPassを用意すればいい。

<?php

namespace App\DependencyInjection\Compiler;


use App\Security\Http\RememberMe\CustomTokenBasedRememberMeServices;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class TokenBasedRememberMePass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        $definition = $container->getDefinition('security.authentication.rememberme.services.simplehash');
        $definition->setClass(CustomTokenBasedRememberMeServices::class);
    }

}

<?php

namespace App\Security\Http\RememberMe;


use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\RememberMe\TokenBasedRememberMeServices;

class CustomTokenBasedRememberMeServices extends TokenBasedRememberMeServices
{
    protected function processAutoLoginCookie(array $cookieParts, Request $request)
    {
        return parent::processAutoLoginCookie($cookieParts, $request); // TODO: Change the autogenerated stub
    }

}
<?php

declare(strict_types=1);

namespace App;

use App\DependencyInjection\Compiler\TokenBasedRememberMePass;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel
{
...
    protected function build(ContainerBuilder $container)
    {
        $container->addCompilerPass(new TokenBasedRememberMePass());
    }
}

RememberMeListenerを差し替えたい

今回はちょっといろいろな事情でRememberMeListenerも変えたくなったので以下のように対応した。

<?php

declare(strict_types=1);

namespace App\Security\Http\Firewall;

use Psr\Log\LoggerInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Http\Firewall\RememberMeListener;
use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Routing\RouterInterface;

class AppRememberMeListener extends RememberMeListener
{
    /**
     * @var RouterInterface
     */
    private $router;

    public function __construct(
        RouterInterface $router,
        TokenStorageInterface $tokenStorage,
        RememberMeServicesInterface $rememberMeServices,
        AuthenticationManagerInterface $authenticationManager,
        LoggerInterface $logger = null,
        EventDispatcherInterface $dispatcher = null,
        bool $catchExceptions = true,
        SessionAuthenticationStrategyInterface $sessionStrategy = null
    ) {
        parent::__construct(
            $tokenStorage,
            $rememberMeServices,
            $authenticationManager,
            $logger,
            $dispatcher,
            $catchExceptions,
            $sessionStrategy
        );
        $this->router = $router;
    }

    public function authenticate(RequestEvent $event): void
    {
        // TODO カスタム処理
        parent::authenticate($event);
    }
}

<?php

declare(strict_types=1);

namespace App\DependencyInjection\Compiler;

use App\Security\Http\Firewall\CustomRememberMeListener;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Routing\RouterInterface;

class CustomRememberMeListenerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        $definition = new Definition(CustomRememberMeListener::class);
        $definition->setArguments(array_values(array_merge([new Reference(RouterInterface::class)], $container->getDefinition('security.authentication.listener.rememberme')->getArguments())));
        $definition->setShared(true);
        $definition->replaceArgument(2, new Reference('security.authentication.rememberme.services.simplehash.main'));
        $container->setDefinition(AppRememberMeListener::class, $definition);
        $container->setDefinition('security.authentication.listener.rememberme.main', $definition);
    }
}

ポイントとしては replaceArgument しているところだと思っていて、ここで security.authentication.rememberme.services.simplehash.main を設定していること。
もともと入っている値 security.authentication.rememberme のままにしていると以下のエラーが出てしまう。

  The service "security.authentication.listener.rememberme.main" has a dependency on a non-existent service "security.authentication.rememberme".

ここはちょっと調査不足なんだけど、原因わからなかったから security.authentication.rememberme.services.simplehash.main に差し替えちゃえという感じ。

※ Firewall名はmainを想定しています。

comments powered by Disqus