symfony4でログイン認証を簡単に実装する

Symfony

Symfonyにはコンソールコマンドにて簡単にログイン認証を実装する事ができます。

この記事ではSymfonyのインストールが完了しているのを前提に説明しています。

前提条件について

以下のcomposerコマンドにてインストールした事を前提とします。

composer create-project symfony/website-skeleton <PROJECT_NAME> 4.*

セキュリティーバンドルのインストール

以下のコマンドを実行してセキュリティーバンドルをインストールします。

composer require symfony/security-bundle

ユーザークラスを作成する

ユーザ認証に使用するためのクラスを作成します。

php bin/console make:user

The name of the security user class (e.g. User) [User]:
>

Do you want to store user data in the database (via Doctrine)? (yes/no) [yes]:
>

Enter a property name that will be the unique "display" name for the user (e.g. email, username, uuid) [email]:
>

Will this app need to hash/check user passwords? Choose No if passwords are not needed or will be checked/hashed by some other system (e.g. a single sign-on server).

Does this app need to hash/check user passwords? (yes/no) [yes]:
>

created: src/Entity/User.php
created: src/Repository/UserRepository.php
updated: src/Entity/User.php
updated: config/packages/security.yaml


 Success!


Next Steps:
  - Review your new App\Entity\User class.
  - Use make:entity to add more fields to your User entity and then run make:migration.
  - Create a way to authenticate! See https://symfony.com/doc/current/security.html

Userクラスのフィールドは任意で追加しても問題ありません。重要なのはSymfony\Component\Security\Core\User\UserInterfaceを継承することです。

クラスを作成したらデータベースを更新します。

php bin/console make:migration
php bin/console doctrine:migrations:migrate

プロバイダの設定

Userクラスに加えて、「user provider」も必要です。セッションからユーザ情報の再読み込みや、なりすまし等などいくつかのオプション機能などを支援してくれます。
しかし、make:userで作成すると、自動でuser providerも作成されます。security.yamlのproviders:を確認してみましょう。

security:
    // ...
    providers:
        # used to reload user from session & other features (e.g. switch_user)
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email

パスワードエンコード

ほとんどのログイン認証には、パスワードが使われます。パスワードは、情報漏洩対策でほとんど暗号化されています。security.yamlにパスワードの暗号方式を設定します。

security:
    encoders:
        App\Entity\User:
            algorithm: auto

次のコマンドを実行すると、パスワードを手動でエンコードできます。

php bin/console security:encode-password

ログインフォーム作成

ログインフォームを作成します。

php bin/console make:auth

 What style of authentication do you want? [Empty authenticator]:
  [0] Empty authenticator
  [1] Login form authenticator
 > 1

 The class name of the authenticator to create (e.g. AppCustomAuthenticator):
 > LoginFormAuthenticator

 Choose a name for the controller class (e.g. SecurityController) [SecurityController]:
 >

 Do you want to generate a '/logout' URL? (yes/no) [yes]:
 >

 created: src/Security/LoginFormAuthenticator.php
 updated: config/packages/security.yaml
 created: src/Controller/SecurityController.php
 created: templates/security/login.html.twig


  Success!


 Next:
 - Customize your new authenticator.
 - Finish the redirect "TODO" in the App\Security\LoginFormAuthenticator::onAuthenticationSuccess() method.
 - Review & adapt the login template: templates/security/login.html.twig.

これにより、1)ログイン/ログアウトルートとコントローラー、2)ログインフォームをレンダリングするテンプレート、3) ログイン送信を処理するGuard認証クラス、4)メインのセキュリティ構成ファイルが更新されます。

1.ログインコントローラー

// src/Controller/SecurityController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

class SecurityController extends AbstractController
{
    /**
     * @Route("/login", name="app_login")
     */
    public function login(AuthenticationUtils $authenticationUtils): Response
    {
        // if ($this->getUser()) {
        //     return $this->redirectToRoute('target_path');
        // }

        // get the login error if there is one
        $error = $authenticationUtils->getLastAuthenticationError();
        // last username entered by the user
        $lastUsername = $authenticationUtils->getLastUsername();

        return $this->render('security/login.html.twig', [
            'last_username' => $lastUsername,
            'error' => $error
        ]);
    }

    /**
     * @Route("/logout", name="app_logout")
     */
    public function logout()
    {
        throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
    }
}

ログアウトのパスをsecurity.yamlに追加します。

2.ログインフォーム

これは、セキュリィーにはほとんど関係がなくログイン用のフォームを用意するだけです。

{% extends 'base.html.twig' %}
{% block title %}Log in!{% endblock %}
{% block body %}
<form method="post">
{% if error %}
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
{% if app.user %}
<div class="mb-3">
You are logged in as {{ app.user.username }}, <a href="{{ path('app_logout') }}">Logout</a>
</div>
{% endif %}
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
<label for="inputEmail">Email</label>
<input type="email" value="{{ last_username }}" name="email" id="inputEmail" class="form-control" required autofocus>
<label for="inputPassword">Password</label>
<input type="password" name="password" id="inputPassword" class="form-control" required>
<input type="hidden" name="_csrf_token"
value="{{ csrf_token('authenticate') }}"
>
{#
Uncomment this section and add a remember_me option below your firewall to activate remember me functionality.
See https://symfony.com/doc/current/security/remember_me.html
<div class="checkbox mb-3">
<label>
<input type="checkbox" name="_remember_me"> Remember me
</label>
</div>
#}
<button class="btn btn-lg btn-primary" type="submit">
Sign in
</button>
</form>
{% endblock %}

3.Guardオーセンティケーターがフォーム送信を処理

namespace App\Security;

use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;

class LoginFormAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface
{
    use TargetPathTrait;

    public const LOGIN_ROUTE = 'app_login';

    private $entityManager;
    private $urlGenerator;
    private $csrfTokenManager;
    private $passwordEncoder;

    public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)
    {
        $this->entityManager = $entityManager;
        $this->urlGenerator = $urlGenerator;
        $this->csrfTokenManager = $csrfTokenManager;
        $this->passwordEncoder = $passwordEncoder;
    }

    public function supports(Request $request)
    {
        return self::LOGIN_ROUTE === $request->attributes->get('_route')
            && $request->isMethod('POST');
    }

    public function getCredentials(Request $request)
    {
        $credentials = [
            'email' => $request->request->get('email'),
            'password' => $request->request->get('password'),
            'csrf_token' => $request->request->get('_csrf_token'),
        ];
        $request->getSession()->set(
            Security::LAST_USERNAME,
            $credentials['email']
        );

        return $credentials;
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        $token = new CsrfToken('authenticate', $credentials['csrf_token']);
        if (!$this->csrfTokenManager->isTokenValid($token)) {
            throw new InvalidCsrfTokenException();
        }

        $user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]);

        if (!$user) {
            // fail authentication with a custom error
            throw new CustomUserMessageAuthenticationException('Email could not be found.');
        }

        return $user;
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
    }

    /**
     * Used to upgrade (rehash) the user's password automatically over time.
     */
    public function getPassword($credentials): ?string
    {
        return $credentials['password'];
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
            return new RedirectResponse($targetPath);
        }

        // For example : return new RedirectResponse($this->urlGenerator->generate('some_route'));
        throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
    }

    protected function getLoginUrl()
    {
        return $this->urlGenerator->generate(self::LOGIN_ROUTE);
    }
}

4.Guardのオーセンティケーターを有効にするためにメインのセキュリティー構成ファイルを更新

security:
    # ...

    firewalls:
        main:
            # ...
            guard:
                authenticators:
                    - App\Security\LoginFormAuthenticator

アクセスコントロール

ログインしないとシステムが使えないように、アクセス権限等を設定する必要があります。しかし、ログインはアクセスを外す必要があります。

security:
    # ...
    access_control:
        - { path: '^/login$', roles: IS_AUTHENTICATED_ANONYMOUSLY }
        # require ROLE_ADMIN for /admin*
        - { path: '^/', roles: ROLE_ADMIN }

次に、ログイン成功時にTOPページに飛ぶよう設定します。ログイン認証時の処理はLoginFormAuthenticatorを変更します。

// src/Security/LoginFormAuthenticator.php

// ...
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
    // ...
    // throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
    return new RedirectResponse($this->urlGenerator->generate('app_homepage'));
}

動作確認

これで、ログイン認証処理は終わりです。では実際に動作確認をする方法を説明します。

事前準備

確認するには予めデータベースへアカウント情報を登録する必要があります。

データベースへ登録するデータはメールアドレスとパスワードの2つになりますが、パスワードは暗号化したハッシュ値を保存します。

暗号化したハッシュ値はパスワードエンコードの項目で説明した以下のコマンドで確認する事ができます。

>php bin/console security:encode-password
Symfony Password Encoder Utility
================================

 Type in your password to be encoded:
 > // パスワードを入力してEnter

 ------------------ ---------------------------------------------------------------------------------------------------
  Key                Value
 ------------------ ---------------------------------------------------------------------------------------------------
  Encoder used       Symfony\Component\Security\Core\Encoder\MigratingPasswordEncoder
  Encoded password   $argon2id$v=19$m=65536,t=4,p=1$d0E5RUNweWlFMWdyWVhubw$QfPYWiHKhw+I7S4bTWPAFj8gSiyismTWad4pHtY3/NA
 ------------------ ---------------------------------------------------------------------------------------------------

 ! [NOTE] Self-salting encoder used: the encoder generated its own built-in salt.


 [OK] Password encoding succeeded

パスワードエンコードのコマンドで出力されたEncode passwordの値をデータベースのパスワードの項目へ登録します。

確認

ブラウザから以下のURLへアクセスし、データベースへ登録したメールアドレスとパスワード(暗号化前の値)を入力して確認します。

http://localhost/login

ログインが正常に完了すると以下のようにデバックツールバーへ登録したメールアドレスの反映が確認できればOKです。

コメント

タイトルとURLをコピーしました