SymfonyのRememberMeとはなんぞや?今まで雰囲気で使ってたので調べてみた。
- RembemerMeとは?
- どのように実現しているのか?
RememberMeとは?
公式ドキュメントによると以下のように記載されている。
ユーザーが認証されると、通常、ユーザーの資格情報がセッションに保存されます。つまり、セッションが終了すると、ユーザーはログアウトされ、次回アプリケーションにアクセスするときに再度ログイン詳細を提供する必要があります。remember_meファイアウォールオプション付きのCookieを使用して、セッションが継続する期間よりも長い間ログインしたままにすることをユーザーが選択できるようにすることができます。
https://symfony.com/doc/current/security/remember_me.html
つまり、セッションが切れてもログイン状態を継続することが出来る。
設定方法について
基本的には security.yaml
に設定を記述していく
1
2
3
4
|
main:
remember_me:
secret: '%kernel.secret%'
lifetime: 31536000
|
ベースはこんな感じ。
パラーメータの種類ついて
- secret(必須)
- name
- lifetime
- path
- domain
- secure
- httponly
- samesite
- remember_me_parameter
- always_remember_me
- 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を用意すればいい。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
<?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);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<?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
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<?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も変えたくなったので以下のように対応した。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
<?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);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
<?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
のままにしていると以下のエラーが出てしまう。
1
|
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を想定しています。