7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Symfony5 + EasyAdmin3 で管理画面を作る

Last updated at Posted at 2020-06-27

はじめに

先日EasyAdminBundleの3系が公開されました。今回は、Symfony5で導入された新しい認証機構を用いてログインフォームを作成し、EasyAdminBundleの3系を利用して管理画面を作成するまでの手順をまとめました。

環境

バージョン
PHP 7.4.7
symfony 5.1.2
easyadmin-bundle 3.0.1

ライブラリのインストール

symfony/apache-packsymfony/profiler-packはデバッグしやすくするために入れているので、なくても大丈夫です。

 $ composer require symfony/apache-pack easycorp/easyadmin-bundle security
 $ composer require --dev symfony/profiler-pack symfony/maker-bundle

Adminクラスの作成

Userクラスの名前だけAdminにして、残りはデフォルトでOK。

$ bin/console make:user

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

 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]:
 > 
...

ログイン機構の作成

続いてログインを処理する認証機構を作成します。make:authで雛形の作成ができるのですが、symfony5で導入された新しい認証機構には対応したインターフェイスでは無いので少し手直しする必要があります。

AdminProviderの作成

まず、一意なID(今回はemail)から対応するAdminクラスのインスタンスを取得するためのAdminProviderを作成します。EntityUserProviderを継承し、コンストラクタでクラス名と一意なIDに対応するプロパティ名を指定することで、とても簡単にProviderを作成することができます。

Serucity/AdminProvider.php
<?php

namespace App\Security;

use App\Entity\Admin;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Bridge\Doctrine\Security\User\EntityUserProvider;

class AdminProvider extends EntityUserProvider
{
    public function __construct(ManagerRegistry $registry, string $classOrAlias = Admin::class, string $property = 'email', string $managerName = null)
    {
        parent::__construct($registry, $classOrAlias, $property, $managerName);
    }
}

Authenticatorの作成

以下のコマンドで、SecurityControllerAdminLoginFormAuthenticatorが作成されます。一つ目の質問は1を選択してください。脳死でエンター連打するとミスります。

 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):
 > AdminLoginForm

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

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

続いて、作成されたAdminLoginFormAuthenticatorを以下のように修正します。
主にやることはこの三つです。

  • 継承する親クラスをAbstractFormLoginAuthenticatorからAbstractLoginFormAuthenticatorへ変更(FormとLoginが逆になっている)
  • ↑によって整合性の取れなくなった関数の戻り値などの型を修正
  • authenticate()メソッドを実装

一番重要なのが最後のauthenticate()メソッドの実装で、ここがsymfony5から導入された認証機構になると思われます。認証に成功した場合、PassportInterfaceと言う物を返すようにします。PassportInterfaceにはBadgeと呼ばれる物を持たせられるようで、これによりRemember meなどを実装できるらしいです。

Security/AdminLoginFormAuthenticator.php
<?php

namespace App\Security;

use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
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\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\Util\TargetPathTrait;

class AdminLoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
    use TargetPathTrait;

    public const LOGIN_ROUTE = 'admin_login';

    private $adminProvider;
    private $urlGenerator;
    private $csrfTokenManager;
    private $passwordEncoder;

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

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

    public function authenticate(Request $request): PassportInterface
    {
        $credentials = $this->getCredentials($request);
        $user = $this->getUser($credentials);

        if (!$user) {
            throw new CustomUserMessageAuthenticationException('Email could not be found.');
        }

        if (!$this->isValidCredentials($credentials, $user)) {
            throw new CustomUserMessageAuthenticationException('Email or Password is invalid.');
        }

        return new SelfValidatingPassport($user);
    }

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

        $url = $this->urlGenerator->generate('admin_dashboard');
        return new RedirectResponse($url);
    }

    public function getLoginUrl(Request $request): string
    {
        return $this->urlGenerator->generate(self::LOGIN_ROUTE);
    }

    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 isValidCredentials($credentials, UserInterface $user)
    {
        return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
    }

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

SecurityControllerはパスのprefixに/adminを追加し、Routeのappをadminへ直しておきます。

SecurityController.php
<?php

namespace App\Controller\Admin;

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
 * @package App\Controller\Admin
 * @Route("/admin")
 */
class SecurityController extends AbstractController
{
    /**
     * @Route("/login", name="admin_login")
     */
    public function login(AuthenticationUtils $authenticationUtils): Response
    {
        $error = $authenticationUtils->getLastAuthenticationError();
        $lastUsername = $authenticationUtils->getLastUsername();
        return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]);
    }

    /**
     * @Route("/logout", name="admin_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の修正

最後にfirewallなどの設定を修正します。 上記の流れで実行すると、デフォルトではmainfirewallの中に認証の設定などが自動で入りますが、管理者向けにfirewallを新たに設定します。

enable_authenticator_managerの項目にtrueを設定することで、symfony5で導入された認証機構を有効化することができます。adminfirewallの中のcustom_authenticatorsに先ほど作成したAdminLoginFormAuthenticatorを指定することで、^/admin/にマッチするURLへリクエストがきた時に認証処理が走ります。認証が通らなかった場合はentry_pointで指定したサービスを用いてレスポンスを返します。ここで指定するサービスはAuthenticationEntryPointInterfaceの実装である必要があるのですが、AdminLoginFormAuthenticatorが継承しているAbstractLoginFormAuthenticatorは既にこの実装が組み込まれているので自動的にログインフォームへリダイレクトしてくれます。

また、従来の認証から変わった点として、「匿名ユーザー」と言う概念がなくなり、代わりに「未認証」として扱われるようになったようです(あまり違いが分かっていない)。そのため、今まで匿名ユーザーのアクセスを許可する場合access_controlroleIS_AUTHENTICATED_ANONYMOUSLYと指定していましたが、新たな認証ではPUBLIC_ACCESSを指定することになったようです。

また、少しハマった点として、firewallの設定にlazy: trueとしないと、access_controlPUBLIC_ACCESSにしても認証処理が走ってしまうようで、ログインフォームへのリダイレクトがループしてしまいます。

packages/security.yaml
security:
    enable_authenticator_manager: true
    encoders:
        App\Entity\Admin:
            algorithm: auto
    providers:
        admin_provider:
            id: App\Security\AdminProvider
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        admin:
            pattern: ^/admin
            lazy: true
            provider: admin_provider
            custom_authenticators:
                - App\Security\AdminLoginFormAuthenticator
            entry_point: App\Security\AdminLoginFormAuthenticator
            logout:
                path: admin_logout
        main:
            security: false


    access_control:
        - { path: ^/admin/login, roles: PUBLIC_ACCESS }
        - { path: ^/admin, roles: ROLE_ADMIN }

ここまでで、admin/loginへアクセスするとログインフォームが表示されるかと思います!

ダミーデータの投入

ダミーデータを投入して実際にログインできるか試してみます。

$ composer require --dev orm-fixtures
DataFixtures/AdminFixtures.php
<?php

namespace App\DataFixtures;

use App\Entity\Admin;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

class AdminFixtures extends Fixture
{
    private $encoder;

    public function __construct(UserPasswordEncoderInterface $encoder)
    {
        $this->encoder = $encoder;
    }

    public function load(ObjectManager $manager)
    {
        $user = new Admin();

        $password = $this->encoder->encodePassword($user, 'password');
        
        $user->setEmail('admin@example.com');
        $user->setPassword($password);
        $user->setRoles(['ROLE_ADMIN']);
        
        $manager->persist($user);
        $manager->flush();
    }
}

データベースの設定は環境に合わせて変えてください。

.env
...
DATABASE_URL=mysql://user:pass@127.0.0.1:3306/sym5admin?serverVersion=5.7
...
$ bin/console d:d:c
Created database `sym5admin` for connection named default

$  bin/console d:s:u -f

$  bin/console doctrine:fixtures:load

 Careful, database "sym5admin" will be purged. Do you want to continue? (yes/no) [no]:
 > yes

以上で、下記のユーザー情報でログインできるかと思います!(/admin のRouteがまだ無いのでエラーが表示されます。)また、異なるパスワードやメールアドレスの場合はフォームへエラーが表示されることも確認できます。
email: admin@example.com
pass: password

easyAdminの設定

easyAdminはほとんど使ったことが無いのですが、2系と3系でかなり設定項目が変わったようです。
まず、easyAdminをインストールすると自動で作成される/config/routes/easy_admin.yamlを削除します。2系だと必要だった見たいですが、3系では必要無い見たいです。

ログインフォームの修正

先ほど修正したSecurityControllerですが、また修正します(笑)。loginメソッドの返り値を以下のようにすることで、easyAdminのテンプレートを用いたかっこいいフォームになります。

SecurityController.php
    /**
     * @Route("/login", name="admin_login")
     */
    public function login(AuthenticationUtils $authenticationUtils): Response
    {
        $error = $authenticationUtils->getLastAuthenticationError();
        $lastUsername = $authenticationUtils->getLastUsername();

        return $this->render('@EasyAdmin/page/login.html.twig', [
            'error' => $error,
            'last_username' => $lastUsername,
            'translation_domain' => 'admin',
            'page_title' => 'ADMIN LOGIN',
            'csrf_token_intention' => 'authenticate',
            'username_label' => 'email',
            'password_label' => 'password',
            'sign_in_label' => 'Log in',
            'username_parameter' => 'email',
            'password_parameter' => 'password',
        ]);
    }

ダッシュボードの作成

easyAdmim3系では、まずダッシュボードを作成することが必要です。以下のコマンドでダッシュボード用のコントローラーが作成されます。この段階で/adminのRouteが作成されるので、ログインするとeasyAdminのダッ種ボードが見れるようになります(まだ何も無いですが)。

bin/console make:admin:dashboard

[OK] Your dashboard class has been successfully generated.

ダッシュボードのメニューの作成

まずはリソースとなるエンティティのCRUDコントローラーを作成します。今回はまだAdminクラスしか無いので、Adminクラスを選択します。(この選択のさせ方はentityの数が増えた時ちょっと大変だなと思った)

 bin/console make:admin:crud

 Which Doctrine entity are you going to manage with this CRUD controller?:
  [0] App\Entity\Admin
 > 0

次にDashBoardControllerconfigureMenuItems()メソッドを次のようにします。

DashBoardController.php

    public function configureMenuItems(): iterable
    {
        return [
            MenuItem::linktoDashboard('Dashboard', 'fa fa-home'),

            MenuItem::Section('Admin'),
            MenuItem::linkToCrud('Admin', 'fa fa-user', Admin::class),
        ];
    }

お疲れ様です!以上で基本的な作業は終わりです!
だいぶ管理画面ぽい見た目になってきたかと思います!

おわりに

今回は、最新のSymfony5 + EasyAdmin3の構成で管理画面を作成する方法をまとめました。Symfonyの管理画面作成用ライブラリではsonataAdminBundleなどが有名ですが、残念ながらSymfony5にはまだ対応していないみたいなので、今回このような記事を作成しました。
また、ログイン処理も今までfosUserbundleなどに頼り切っていたため、素直な実装を改めてしてみたことでかなり勉強になりました。EasyAdmin3はまだリリースされたばかりであまり情報が無いですが、これからじっくり触ってみようと思います。

7
5
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?