はじめに
公式ドキュメントのSecurityとHow to Build a Login Formを見ながら、ログインフォームでログインをするサンプルを作成しました。
その続きとして、ログインに失敗した場合に、失敗した人のログを取得しておきたいと思います。
方針
How to Build a Login Formで作成した場合、LoginFormAuthenticator.php
にLoggerをインジェクションして出力したくなります。
これをしちゃうと、認証という機能にロギングという別の機能が混じってしまうので、よろしくないようです。
Symfonyの定石としては、認証終了後にAuthentication Eventsが送信されるので、このイベントを待ち受けて処理をするのがよいようです。
イベントリスナーの作成
イベントリスナークラスの雛形の作成
Events and Event Listenersを読みながらイベントリスナーを作成します。
まず、src
配下に、EventListener
ディレクトリを作成します。
そのディレクトリ配下に、LoginLoggingListener
クラスを作成します。
以下が、LoginLoggingListener
クラスの骨組みになります。
EventSubscriberInterface
を継承します。
<?php
namespace App\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class LoginLoggingListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
// TODO: Implement getSubscribedEvents() method.
}
}
リッスンするイベントを登録する
イベントの購読登録は、Creating an Event Listenerを見ていくと、service.yaml
に記載する方法と、Creating an Event Subscriberを実装していく方法があります。
今回は、Event Subscriber
を実装していきます。
Authentication Eventsを見ると、ログインに失敗した場合は、security.authentication.failure
のイベントが起きますので、これをフックすると良さそうです。
フック関数として、loggingLoginFailure
メソッドを登録します。2つ目の10
というのは、このイベントに複数のフック関数を登録する場合の優先順となる番号になります。
class LoginLoggingListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
AuthenticationEvents::AUTHENTICATION_FAILURE => [
['loggingLoginFailure', 10]
]
];
}
}
イベントフック関数の実装
フック関数の実態を作成していきます。
フック関数には、AuthenticationFailureEvent
が渡されますが、欲しい情報は入っているのでしょうか?
一旦、ddして見てみます。
class LoginLoggingListener implements EventSubscriberInterface
{
~~~ 省略 ~~~
public function loggingLoginFailure(AuthenticationFailureEvent $event):void
{
dd($event);
}
}
こんな感じで、ログインフォームに入力された情報のメールアドレス、パスワードは取れました!
しかし、肝心のクライアントのIPなどは入っていません。
ControllerのようにRequest
を取得すればよいかと試しましたが、うまくいきませんでした。
ぐぐってみると、同じことをしたい人がおられました。
Symfony - How to get username and IP address in authentication failure listener?
どうやら、RequestStack
をインジェクションするとよいようです。
このあたり、どうして、RequestStack
がよいのか、わからないので、誰か詳しい方に教えていただきたいところです。
最終的に、LoggerInterface
もインジェクトして、以下のようになりました。
class LoginLoggingListener implements EventSubscriberInterface
{
private $logger;
private $requestStack;
public function __construct(LoggerInterface $loginAuditLogger, RequestStack $requestStack)
{
$this->logger = $loginAuditLogger;
$this->requestStack = $requestStack;
}
public static function getSubscribedEvents()
{
return [
AuthenticationEvents::AUTHENTICATION_FAILURE => [
['LoggingLoginFailure', 10]
]
];
}
public function loggingLoginFailure(AuthenticationFailureEvent $event):void
{
$hCredentials = $event->getAuthenticationToken()->getCredentials();
$this->logger->notice(sprintf('[Login Failure] email: %s, password: %s, ip: %s, ua: %s, referer: %s',
$hCredentials['email'],
$hCredentials['password'],
$request->getClientIp(),
$request->headers->get('user-agent'),
$request->headers->get('referer')
));
}
}
今回の趣旨と異なるのですが、ログは独自ファイル(login_audit-xxxx.log)に出力しています。
[2019-12-24 16:43:15] login_audit.NOTICE: [Login Failure] email: idani@hirotae.com, password: hogehoge, ip: 172.18.0.1, ua: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36, referer: https://localhost/login [] []
まとめ
Symfonyには、様々なイベントがあるので、それを使って、機能を最小化していくと良さそうですね。
そろそろサービスとかバンドルとか、理解しづらい部分がでてきたので、気軽に日本語で相談できるメンターが欲しいところです。