Symfonyにはコンソールコマンドにて簡単にログイン認証を実装する事ができます。
この記事ではSymfonyのインストールが完了しているのを前提に説明しています。
セキュリティーバンドルのインストール
以下のコマンドを実行してセキュリティーバンドルをインストールします。
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
コメント