在 Symfony5.3 上自定义新的身份验证错误消息
Customize new authentication errors messages on Symfony5.3
在我的新 symfony 5.3 项目中,我刚刚实施了新的身份验证系统,它运行良好,但我的问题是我无法自定义身份验证错误:
在方法中:onAuthentificationFailure在AbstractLoginFormAuthenticator
但在我看来,它只显示会话错误,这是正常的,因为我的控制器调用了 getLastAuthenticationError() 方法。
但是我如何显示我的自定义错误
CustomUserMessageAuthenticationException
在我看来?
我的 AbstractLoginFormAuthenticator
namespace Symfony\Component\Security\Http\Authenticator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
/**
* A base class to make form login authentication easier!
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface, InteractiveAuthenticatorInterface
{
/**
* Return the URL to the login page.
*/
abstract protected function getLoginUrl(Request $request): string;
/**
* {@inheritdoc}
*
* Override to change the request conditions that have to be
* matched in order to handle the login form submit.
*
* This default implementation handles all POST requests to the
* login path (@see getLoginUrl()).
*/
public function supports(Request $request): bool
{
return $request->isMethod('POST') && $this->getLoginUrl($request) === $request->getPathInfo();
}
/**
* Override to change what happens after a bad username/password is submitted.
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
{
if ($request->hasSession()) {
//$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
throw new CustomUserMessageAuthenticationException('error custom ');
}
$url = $this->getLoginUrl($request);
return new RedirectResponse($url);
}
/**
* Override to control what happens when the user hits a secure page
* but isn't logged in yet.
*/
public function start(Request $request, AuthenticationException $authException = null): Response
{
$url = $this->getLoginUrl($request);
return new RedirectResponse($url);
}
public function isInteractive(): bool
{
return true;
}
}
我的安全控制器:
namespace App\Controller;
use App\Entity\User;
use DateTimeImmutable;
use App\Form\RegistrationType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class SecurityController extends AbstractController {
/**
* @Route("/test", name="test")
*/
public function test(Request $request, Response $response){
return $this->render('test.html.twig');
dump($response);
}
/**
* @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(): void
{
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
}
/**
* @Route("/Inscription", name="registration")
*/
public function registration(EntityManagerInterface $manager, Request $request, UserPasswordHasherInterface $passwordEncoder): Response
{
$user = new User;
$registrationForm = $this->createForm(RegistrationType::class, $user);
$registrationForm->handleRequest($request);
if($registrationForm->isSubmitted() && $registrationForm->isValid()){
$plainPassword = $registrationForm->getData()->getPassword();
$user->setPassword($passwordEncoder->hashPassword($user, $plainPassword));
$user->setCreatedAt( new \DateTimeImmutable('NOW'));
$user->setRoles($user->getRoles());
$manager->persist($user);
$manager->flush();
}
return $this->render('security/registration.html.twig',
['registrationForm' => $registrationForm->createView()]);
}
}
我的视图树枝(登录表单):
{% 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" autocomplete="email" required autofocus>
<label for="inputPassword">Password</label>
<input type="password" name="password" id="inputPassword" class="form-control" autocomplete="current-password" 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 %}
您应该扩展和覆盖 AbstractLoginFormAuthenticator
而不是直接修改它。
为什么你的方法不起作用
简而言之,您需要在到达 onAuthenticationFailure()
之前抛出异常,因为 AuthenticationException
是 Symfony 调用 onAuthenticationFailure()
的原因。
onAuthenticationFailure()
方法处理 AuthenticatorManager::executeAuthenticator()
进程抛出的 AuthenticationException
。
try {
// get the passport from the Authenticator
$passport = $authenticator->authenticate($request);
//...
} catch (AuthenticationException $e) {
// oh no! Authentication failed!
$response = $this->handleAuthenticationFailure($e, $request, $authenticator, $passport);
// ...
}
//...
private function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $authenticator, ?PassportInterface $passport): ?Response
{
// Avoid leaking error details in case of invalid user (e.g. user not found or invalid account status)
// to prevent user enumeration via response content comparison
if ($this->hideUserNotFoundExceptions && ($authenticationException instanceof UsernameNotFoundException || ($authenticationException instanceof AccountStatusException && !$authenticationException instanceof CustomUserMessageAccountStatusException))) {
$authenticationException = new BadCredentialsException('Bad credentials.', 0, $authenticationException);
}
$response = $authenticator->onAuthenticationFailure($request, $authenticationException);
//...
}
AbstractLoginFormAuthenticator::onAuthenticationFailure()
的默认功能使用 $request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
,这是从异常中添加消息的内容,通过调用 AuthenticationUtils::getLastAuthenticationError();
检索 $session->get(Security::AUTHENTICATION_ERROR);
public function getLastAuthenticationError(bool $clearSession = true)
{
$request = $this->getRequest();
$authenticationException = null;
//...
if ($request->hasSession() && ($session = $request->getSession())->has(Security::AUTHENTICATION_ERROR)) {
$authenticationException = $session->get(Security::AUTHENTICATION_ERROR);
//...
}
return $authenticationException;
}
如何显示自定义错误消息
应用程序中有多个调用点,您可以在其中抛出传递给 onAuthenticationFailure()
的异常。
默认情况下,Symfony 使用安全配置设置来生成通用消息,但可以通过在 类.
中实现它们来覆盖
由于异常被上述条件覆盖。
if ($this->hideUserNotFoundExceptions && !$e instanceof CustomUserMessageAccountStatusException) {
throw new BadCredentialsException('Bad credentials.', 0, $e);
}
您需要禁用 hide_user_not_found
,否则 CustomUserMessageAuthenticationException
将被替换为 BadCredentialsException('Bad credentials.')
异常。
否则跳过禁用 hide_user_not_found
并抛出 CustomUserMessageAccountStatusException
而不是 CustomUserMessageAuthenticationException
。
# /config/packages/security.yaml
security:
# ...
hide_user_not_found: false
firewalls:
# ...
有关最新的用法参考,请阅读默认值 FormLoginAuthenticator
。由于 Symfony Casts 站点上的教程尚未针对 Symfony 5 进行更新。
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
class MyLoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
//...
public function getCredentials(Request $request)
{
//...
throw new CustomUserMessageAuthenticationException('Custom Error');
}
public function authenticate(Request $request): PassportInterface
{
//general usage of how the other exceptions are thrown here
$credentials = $this->getCredentials($request);
$method = 'loadUserByIdentifier';
$badge = new UserBadge($credentials['username'], [$this->userProvider, $method]),
$user = $badge->getUser(); //UserRepository::loadUserByIdentifier()
//...
throw new CustomUserMessageAuthenticationException('Custom Error');
}
}
class UserRepository extends ServiceEntityRepository implements UserProviderInterface
{
//....
public function loadUserByIdentifier(string $identifier)
{
throw new CustomUserMessageAuthenticationException('Custom Error');
//...
}
public function loadUserByUsername(string $username)
{
return $this->loadUserByIdentifier($username);
}
}
use Symfony\Component\Security\Core\User\{UserCheckerInterface, UserInterface};
class UserChecker implements UserCheckerInterface
{
public function checkPreAuth(UserInterface $user): void
{
throw new CustomUserMessageAuthenticationException('Custom Error');
//...
}
public function checkPostAuth(UserInterface $user): void
{
throw new CustomUserMessageAuthenticationException('Custom Error');
//...
}
}
在我的新 symfony 5.3 项目中,我刚刚实施了新的身份验证系统,它运行良好,但我的问题是我无法自定义身份验证错误:
在方法中:onAuthentificationFailure在AbstractLoginFormAuthenticator 但在我看来,它只显示会话错误,这是正常的,因为我的控制器调用了 getLastAuthenticationError() 方法。
但是我如何显示我的自定义错误 CustomUserMessageAuthenticationException 在我看来?
我的 AbstractLoginFormAuthenticator
namespace Symfony\Component\Security\Http\Authenticator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
/**
* A base class to make form login authentication easier!
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface, InteractiveAuthenticatorInterface
{
/**
* Return the URL to the login page.
*/
abstract protected function getLoginUrl(Request $request): string;
/**
* {@inheritdoc}
*
* Override to change the request conditions that have to be
* matched in order to handle the login form submit.
*
* This default implementation handles all POST requests to the
* login path (@see getLoginUrl()).
*/
public function supports(Request $request): bool
{
return $request->isMethod('POST') && $this->getLoginUrl($request) === $request->getPathInfo();
}
/**
* Override to change what happens after a bad username/password is submitted.
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
{
if ($request->hasSession()) {
//$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
throw new CustomUserMessageAuthenticationException('error custom ');
}
$url = $this->getLoginUrl($request);
return new RedirectResponse($url);
}
/**
* Override to control what happens when the user hits a secure page
* but isn't logged in yet.
*/
public function start(Request $request, AuthenticationException $authException = null): Response
{
$url = $this->getLoginUrl($request);
return new RedirectResponse($url);
}
public function isInteractive(): bool
{
return true;
}
}
我的安全控制器:
namespace App\Controller;
use App\Entity\User;
use DateTimeImmutable;
use App\Form\RegistrationType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class SecurityController extends AbstractController {
/**
* @Route("/test", name="test")
*/
public function test(Request $request, Response $response){
return $this->render('test.html.twig');
dump($response);
}
/**
* @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(): void
{
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
}
/**
* @Route("/Inscription", name="registration")
*/
public function registration(EntityManagerInterface $manager, Request $request, UserPasswordHasherInterface $passwordEncoder): Response
{
$user = new User;
$registrationForm = $this->createForm(RegistrationType::class, $user);
$registrationForm->handleRequest($request);
if($registrationForm->isSubmitted() && $registrationForm->isValid()){
$plainPassword = $registrationForm->getData()->getPassword();
$user->setPassword($passwordEncoder->hashPassword($user, $plainPassword));
$user->setCreatedAt( new \DateTimeImmutable('NOW'));
$user->setRoles($user->getRoles());
$manager->persist($user);
$manager->flush();
}
return $this->render('security/registration.html.twig',
['registrationForm' => $registrationForm->createView()]);
}
}
我的视图树枝(登录表单):
{% 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" autocomplete="email" required autofocus>
<label for="inputPassword">Password</label>
<input type="password" name="password" id="inputPassword" class="form-control" autocomplete="current-password" 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 %}
您应该扩展和覆盖 AbstractLoginFormAuthenticator
而不是直接修改它。
为什么你的方法不起作用
简而言之,您需要在到达 onAuthenticationFailure()
之前抛出异常,因为 AuthenticationException
是 Symfony 调用 onAuthenticationFailure()
的原因。
onAuthenticationFailure()
方法处理 AuthenticatorManager::executeAuthenticator()
进程抛出的 AuthenticationException
。
try {
// get the passport from the Authenticator
$passport = $authenticator->authenticate($request);
//...
} catch (AuthenticationException $e) {
// oh no! Authentication failed!
$response = $this->handleAuthenticationFailure($e, $request, $authenticator, $passport);
// ...
}
//...
private function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $authenticator, ?PassportInterface $passport): ?Response
{
// Avoid leaking error details in case of invalid user (e.g. user not found or invalid account status)
// to prevent user enumeration via response content comparison
if ($this->hideUserNotFoundExceptions && ($authenticationException instanceof UsernameNotFoundException || ($authenticationException instanceof AccountStatusException && !$authenticationException instanceof CustomUserMessageAccountStatusException))) {
$authenticationException = new BadCredentialsException('Bad credentials.', 0, $authenticationException);
}
$response = $authenticator->onAuthenticationFailure($request, $authenticationException);
//...
}
AbstractLoginFormAuthenticator::onAuthenticationFailure()
的默认功能使用 $request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
,这是从异常中添加消息的内容,通过调用 AuthenticationUtils::getLastAuthenticationError();
检索 $session->get(Security::AUTHENTICATION_ERROR);
public function getLastAuthenticationError(bool $clearSession = true)
{
$request = $this->getRequest();
$authenticationException = null;
//...
if ($request->hasSession() && ($session = $request->getSession())->has(Security::AUTHENTICATION_ERROR)) {
$authenticationException = $session->get(Security::AUTHENTICATION_ERROR);
//...
}
return $authenticationException;
}
如何显示自定义错误消息
应用程序中有多个调用点,您可以在其中抛出传递给 onAuthenticationFailure()
的异常。
默认情况下,Symfony 使用安全配置设置来生成通用消息,但可以通过在 类.
中实现它们来覆盖由于异常被上述条件覆盖。
if ($this->hideUserNotFoundExceptions && !$e instanceof CustomUserMessageAccountStatusException) {
throw new BadCredentialsException('Bad credentials.', 0, $e);
}
您需要禁用 hide_user_not_found
,否则 CustomUserMessageAuthenticationException
将被替换为 BadCredentialsException('Bad credentials.')
异常。
否则跳过禁用 hide_user_not_found
并抛出 CustomUserMessageAccountStatusException
而不是 CustomUserMessageAuthenticationException
。
# /config/packages/security.yaml
security:
# ...
hide_user_not_found: false
firewalls:
# ...
有关最新的用法参考,请阅读默认值 FormLoginAuthenticator
。由于 Symfony Casts 站点上的教程尚未针对 Symfony 5 进行更新。
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
class MyLoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
//...
public function getCredentials(Request $request)
{
//...
throw new CustomUserMessageAuthenticationException('Custom Error');
}
public function authenticate(Request $request): PassportInterface
{
//general usage of how the other exceptions are thrown here
$credentials = $this->getCredentials($request);
$method = 'loadUserByIdentifier';
$badge = new UserBadge($credentials['username'], [$this->userProvider, $method]),
$user = $badge->getUser(); //UserRepository::loadUserByIdentifier()
//...
throw new CustomUserMessageAuthenticationException('Custom Error');
}
}
class UserRepository extends ServiceEntityRepository implements UserProviderInterface
{
//....
public function loadUserByIdentifier(string $identifier)
{
throw new CustomUserMessageAuthenticationException('Custom Error');
//...
}
public function loadUserByUsername(string $username)
{
return $this->loadUserByIdentifier($username);
}
}
use Symfony\Component\Security\Core\User\{UserCheckerInterface, UserInterface};
class UserChecker implements UserCheckerInterface
{
public function checkPreAuth(UserInterface $user): void
{
throw new CustomUserMessageAuthenticationException('Custom Error');
//...
}
public function checkPostAuth(UserInterface $user): void
{
throw new CustomUserMessageAuthenticationException('Custom Error');
//...
}
}