はじめに
先日EasyAdminBundle
の3系が公開されました。今回は、Symfony5で導入された新しい認証機構を用いてログインフォームを作成し、EasyAdminBundle
の3系を利用して管理画面を作成するまでの手順をまとめました。
環境
バージョン | |
---|---|
PHP | 7.4.7 |
symfony | 5.1.2 |
easyadmin-bundle | 3.0.1 |
ライブラリのインストール
symfony/apache-pack
とsymfony/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を作成することができます。
<?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の作成
以下のコマンドで、SecurityController
とAdminLoginFormAuthenticator
が作成されます。一つ目の質問は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
などを実装できるらしいです。
<?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へ直しておきます。
<?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などの設定を修正します。 上記の流れで実行すると、デフォルトではmain
firewallの中に認証の設定などが自動で入りますが、管理者向けにfirewallを新たに設定します。
enable_authenticator_manager
の項目にtrueを設定することで、symfony5で導入された認証機構を有効化することができます。admin
firewallの中のcustom_authenticators
に先ほど作成したAdminLoginFormAuthenticator
を指定することで、^/admin/にマッチするURLへリクエストがきた時に認証処理が走ります。認証が通らなかった場合はentry_point
で指定したサービスを用いてレスポンスを返します。ここで指定するサービスはAuthenticationEntryPointInterface
の実装である必要があるのですが、AdminLoginFormAuthenticator
が継承しているAbstractLoginFormAuthenticator
は既にこの実装が組み込まれているので自動的にログインフォームへリダイレクトしてくれます。
また、従来の認証から変わった点として、「匿名ユーザー」と言う概念がなくなり、代わりに「未認証」として扱われるようになったようです(あまり違いが分かっていない)。そのため、今まで匿名ユーザーのアクセスを許可する場合access_control
のrole
にIS_AUTHENTICATED_ANONYMOUSLY
と指定していましたが、新たな認証ではPUBLIC_ACCESS
を指定することになったようです。
また、少しハマった点として、firewallの設定にlazy: true
としないと、access_control
でPUBLIC_ACCESS
にしても認証処理が走ってしまうようで、ログインフォームへのリダイレクトがループしてしまいます。
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
<?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();
}
}
データベースの設定は環境に合わせて変えてください。
...
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のテンプレートを用いたかっこいいフォームになります。
/**
* @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
次にDashBoardController
のconfigureMenuItems()
メソッドを次のようにします。
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はまだリリースされたばかりであまり情報が無いですが、これからじっくり触ってみようと思います。