Symfony4でログインに失敗した場合にログに出力する

# はじめに

公式ドキュメントの[Security](https://symfony.com/doc/4.4/security.html)と[How to Build a Login Form](https://symfony.com/doc/4.4/security/form_login_setup.html)を見ながら、ログインフォームでログインをするサンプルを作成しました。

その続きとして、ログインに失敗した場合に、失敗した人のログを取得しておきたいと思います。


# 方針

[How to Build a Login Form](https://symfony.com/doc/4.4/security/form_login_setup.html)で作成した場合、<code>LoginFormAuthenticator.php</code>にLoggerをインジェクションして出力したくなります。

これをしちゃうと、**認証**という機能に**ロギング**という別の機能が混じってしまうので、よろしくないようです。

Symfonyの定石としては、認証終了後に[Authentication Events](https://symfony.com/doc/current/components/security/authentication.html#authentication-events)が送信されるので、このイベントを待ち受けて処理をするのがよいようです。

# イベントリスナーの作成

## イベントリスナークラスの雛形の作成

[Events and Event Listeners](https://symfony.com/doc/4.4/event_dispatcher.html)を読みながらイベントリスナーを作成します。

まず、<code>src</code>配下に、<code>EventListener</code>ディレクトリを作成します。
そのディレクトリ配下に、<code>LoginLoggingListener</code>クラスを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/77cfbd8f-9fed-94e4-378e-b7c358b5f8c6.png)

以下が、<code>LoginLoggingListener</code>クラスの骨組みになります。
<code>EventSubscriberInterface</code>を継承します。

```php:/src/EventListener/LoginLoggingListener.php
<?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](https://symfony.com/doc/4.4/event_dispatcher.html#creating-an-event-listener)を見ていくと、<code>service.yaml</code>に記載する方法と、[Creating an Event Subscriber](https://symfony.com/doc/4.4/event_dispatcher.html#creating-an-event-subscriber)を実装していく方法があります。

今回は、<code>Event Subscriber</code>を実装していきます。

[Authentication Events](https://symfony.com/doc/current/components/security/authentication.html#authentication-events)を見ると、ログインに失敗した場合は、<code>security.authentication.failure</code>のイベントが起きますので、これをフックすると良さそうです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/a637afe7-4342-3e3c-8079-7ab252afb003.png)

フック関数として、<code>loggingLoginFailure</code>メソッドを登録します。2つ目の<code>10</code>というのは、このイベントに複数のフック関数を登録する場合の優先順となる番号になります。

```php:/src/EventListener/LoginLoggingListener.php
class LoginLoggingListener implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [
            AuthenticationEvents::AUTHENTICATION_FAILURE => [
                ['loggingLoginFailure', 10]
            ]
        ];
    }
}
```

## イベントフック関数の実装

フック関数の実態を作成していきます。
フック関数には、<code>AuthenticationFailureEvent</code>が渡されますが、欲しい情報は入っているのでしょうか？

一旦、ddして見てみます。

```php:/src/EventListener/LoginLoggingListener.php
class LoginLoggingListener implements EventSubscriberInterface
{
～～～　省略 ～～～

    public function loggingLoginFailure(AuthenticationFailureEvent $event):void
    {
　　　dd($event);
    }
}
```

こんな感じで、ログインフォームに入力された情報のメールアドレス、パスワードは取れました！
しかし、肝心のクライアントのIPなどは入っていません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/eeca1f3d-0d57-73df-7e58-d47155334a8e.png)


Controllerのように<code>Request</code>を取得すればよいかと試しましたが、うまくいきませんでした。

ぐぐってみると、同じことをしたい人がおられました。

[Symfony - How to get username and IP address in authentication failure listener?](https://stackoverflow.com/questions/38693147/symfony-how-to-get-username-and-ip-address-in-authentication-failure-listener)

どうやら、<code>RequestStack</code>をインジェクションするとよいようです。
このあたり、どうして、<code>RequestStack</code>がよいのか、わからないので、誰か詳しい方に教えていただきたいところです。


最終的に、<code>LoggerInterface</code>もインジェクトして、以下のようになりました。

```php:/src/EventListener/LoginLoggingListener.php
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)に出力しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/b94154e1-8324-12f5-8513-aecbfa7b223b.png)

```log:/var/log/login_audit-2019-12-24.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には、様々なイベントがあるので、それを使って、機能を最小化していくと良さそうですね。

そろそろサービスとかバンドルとか、理解しづらい部分がでてきたので、気軽に**日本語で**相談できるメンターが欲しいところです。