symfony之form_login_out的几个事件: 之前简单记录了form_login/out的定义方式,今天再仔细探究一下其中的细节
1.在Symfony上,使用内置的登录系统
①登录form login.html.twig
{% extends 'base.html.twig' %}
{% block title %}Login{% endblock %}
{% block base_stylesheets %}
{{ encore_entry_link_tags('user_registers') }}
{% endblock %}
{% block body %}
<div class="admin-login-wrap">
<form class="admin-form-login" method="post" action="{{ path('login_check') }}" novalidate autocomplete="off">
<input type="hidden" name="_csrf_token"
value="{{ csrf_token('authenticate') }}"></input>
{% for message in app.session.flashbag.get('notice') %}
<div class="text-center mb-4">
<h6 class="mb-3" style="font-size: 12px;color:red">{{ message }}</h6>
</div>
{% endfor %}
<div class="text-center mb-4">
<h1 class="mb-3 login-title">Login With Email </h1>
</div>
<div class="form-label-group">
<input type="text" id="inputEmail" name="email" class="form-control" placeholder="Email" required="" autofocus="" value="">
</div>
<div class="form-label-group">
<input type="password" id="inputPassword" name="password" class="form-control" placeholder="Password" required="" autofocus="" value="">
</div>
<button class="btn btn-lg btn-block" type="submit">Login</button>
</form>
</div>
{% endblock %}
{% block base_javascripts %}
{# {{ encore_entry_script_tags('user_registers') }}#}
{% endblock %}
②控制器
SecurityController
<?php
namespace App\Controller\Front;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class SecurityController extends AbstractController
{
#[Route('/login', name: 'login_form')]
public function showLogin(Request $request): Response
{
if (!$this->getParameter('sso_disable')) {
if ($this->isGranted('IS_AUTHENTICATED_FULLY')) {
return $this->redirectToRoute('front_index');
} else {
throw $this->createAccessDeniedException();
}
}
if ($this->isGranted('IS_AUTHENTICATED_FULLY')) {
return $this->redirectToRoute('front_index');
}
return $this->render('security/index.html.twig');
}
#[Route('/login_check', name: 'login_check', methods: ['post'])]
public function loginCheck(Request $request)
{
return new Response();
}
#[Route('/logout', name: 'logout')]
public function logOut(Request $request): void
{
}
}
③security.yaml配置文件
security:
enable_authenticator_manager: false
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
# Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
App\Entity\User:
algorithm: bcrypt
cost: 4
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
main:
entity:
class: App\Entity\User
property: email
users_in_memory: { memory: null }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
pattern: ^/
provider: main
form_login:
success_handler: App\Security\LoginSuccessHandler #用于登录成功的钩子函数
failure_handler: App\Security\LoginFailHandler #用于登录失败的钩子函数
username_parameter: email
password_parameter: password
sso:
require_previous_session: false
provider: main
check_path: /otp/validate/ # Same as in app/config/routing.yml
sso_scheme: "%idp_scheme%" # Required
sso_host: "%idp_url%" # Required
sso_otp_scheme: "%qinghong_scheme%" # Optional
sso_otp_host: "%qinghong_domain%" # Optional
sso_failure_path: /login
sso_path: /sso/login/ # SSO endpoint on IdP.
sso_service: "%sso_service%" # Consumer name
success_handler: App\Security\LoginSuccessHandler
logout:
invalidate_session: true
path: /logout
# success_handler: App\Security\LogoutSuccessHandler #这个是退出登录的钩子函数,不过官方推荐用LogoutEvent,后面会介绍这个方法
security: true
anonymous: true
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/api, roles: ROLE_USER }
- { path: /.*, role: PUBLIC_ACCESS }
when@test:
security:
password_hashers:
# By default, password hashers are resource intensive and take time. This is
# important to generate secure password hashes. In tests however, secure hashes
# are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon
登录相关:
App\Security目录下创建如下handler
LoginSuccessHandler.php
<?php
namespace App\Security;
use App\Entity\User;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
class LoginSuccessHandler extends DefaultAuthenticationSuccessHandler
{
public function onAuthenticationSuccess(Request $request, TokenInterface $token): RedirectResponse|Response
{
/** @var User $user */
$user = $token->getUser();
$userInfo = [];
if ($user) {
$userInfo['id'] = $user->getId();
$userInfo['email'] = $user->getEmail();
$userInfo['firstName'] = $user->getFirstName();
$userInfo['middleName'] = $user->getMiddleName();
$userInfo['lastName'] = $user->getLastName();
$userInfo['isAdmin'] = count($user->getRoles()) > 1 ? 1 : 0;
$userInfo['image'] = $user->getImage();
$userInfo['avatar'] = $user->getAvatar();
}
$response = $this->httpUtils->createRedirectResponse($request, $this->determineTargetUrl($request));
$cookie = new Cookie('userInfo', json_encode($userInfo), 0, '/', null, null, false);
$response->headers->setCookie($cookie);
return $response;
}
}
LoginFailHandler.php
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationFailureHandler;
class LoginFailHandler extends DefaultAuthenticationFailureHandler
{
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): \Symfony\Component\HttpFoundation\RedirectResponse|Response
{
//要想在后续页面获取到session信息,必须要通过$request->getSession()获取当前ctx的sesson信息,而不是直接new session(),这样会有问题
$request->getSession()->getFlashBag()->add(
'notice',//这里很奇怪,只有type为notice的时候,再次跳转到上面 login.html.twig 通过 app.session.flashbag.get('notice') 可以获取导数据,其它type 怎么都获取不到数据
$exception->getMessage()
);
return $this->httpUtils->createRedirectResponse($request, '/login');
}
}
退出登录相关:
App\Security目录下创建如下如下handler,通过handler实现,不过这种方式已被遗弃,不推荐使用
LogoutSuccessHandler.php //这个在security.yaml的logout下直接配置
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface;
class LogoutSuccessHandler implements LogoutSuccessHandlerInterface
{
public function onLogoutSuccess(Request $request)
{
file_put_contents('./3.txt', time());
$response = new RedirectResponse('/');
$cookie = new Cookie('userInfo', null, 0, '/', null, null, false);
$response->headers->setCookie($cookie);
return $response;
}
}
也可以在services.yaml下添加如下配置,通过listener实现:
App\EventListener\LogoutSuccessListener:
arguments:
$idp_scheme: "%idp_scheme%"
$idp_url: "%idp_url%"
tags:
- name: 'kernel.event_listener'
event: 'Symfony\Component\Security\Http\Event\LogoutEvent'
dispatcher: security.event_dispatcher.main
LogoutSuccessListener.php
<?php
namespace App\EventListener;
use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent;
class LogoutSuccessListener
{
protected $idp_url;
private UrlGeneratorInterface $urlGenerator;
private ContainerBagInterface $params;
public function __construct($idp_scheme, $idp_url, UrlGeneratorInterface $urlGenerator, ContainerBagInterface $params)
{
$this->idp_url = $idp_scheme . "://" . $idp_url;
$this->urlGenerator = $urlGenerator;
$this->params = $params;
}
//这个方法来源于社区pr
public function onSymfonyComponentSecurityHttpEventLogoutEvent(LogoutEvent $param): void
{
$sso_disable = $this->params->get('sso_disable');
if (!$sso_disable) {
$url = $this->idp_url . '/sso/logout?service=qinghong';
} else {
$url = '/';
}
$response = new RedirectResponse($url);
$cookie = new Cookie('userInfo', null, 0, '/', null, null, false);
$response->headers->setCookie($cookie);
$param->setResponse($response);
}
}
还可以通过订阅 LogoutEvent 实现,这样就不用在services.yaml下添加额外配置:
LogoutSubscriber.php
<?php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Http\Event\LogoutEvent;
class LogoutSubscriber implements EventSubscriberInterface
{
public function onLogout(LogoutEvent $logoutEvent): void
{
file_put_contents('./4.txt', time());
$response = new RedirectResponse('/');
$cookie = new Cookie('userInfo', null, 0, '/', null, null, false);
$response->headers->setCookie($cookie);
$logoutEvent->setResponse($response);
}
public static function getSubscribedEvents(): array
{
return [
LogoutEvent::class => 'onLogout',
];
}
}
参考:https://stackoverflow.com/questions/60998790/symfony-5confirmation-message-after-logout
2.自定义
security.yaml的配置如下
security:
enable_authenticator_manager: true #此时要配置为true
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
# Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
App\Entity\User:
algorithm: bcrypt
cost: 4
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
main:
entity:
class: App\Entity\User
property: email
users_in_memory: { memory: null }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
# lazy: true
pattern: ^/
provider: main
# form_login: true #这个可以不要,直接下面这个 custom_authenticator 即可
custom_authenticator: App\Security\FormLoginAuthenticator
logout:
invalidate_session: true
path: /logout
security: true
# anonymous: true
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/api, roles: ROLE_USER }
- { path: /.*, role: IS_AUTHENTICATED_ANONYMOUSLY }
when@test:
security:
password_hashers:
# By default, password hashers are resource intensive and take time. This is
# important to generate secure password hashes. In tests however, secure hashes
# are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon
FormLoginAuthenticator.php
<?php
namespace App\Security;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class FormLoginAuthenticator extends AbstractLoginFormAuthenticator
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'login_form';//login form 路由,有异常可以自动跳转到这个地址
private UrlGeneratorInterface $urlGenerator;
private UserPasswordHasherInterface $userPasswordHasher;
public function __construct(UrlGeneratorInterface $urlGenerator, UserPasswordHasherInterface $userPasswordHasher, private EntityManagerInterface $em,)
{
$this->urlGenerator = $urlGenerator;
$this->userPasswordHasher = $userPasswordHasher;
}
public function supports(Request $request): bool
{
return $request->attributes->get('_route') === 'login_success';//这个是验证submit的post路由
}
/**
*验证用户名和密码是否正确
* @throws \Exception
*/
public function authenticate(Request $request): Passport
{
$username = $request->request->get('email', '');
$request->getSession()->set(Security::LAST_USERNAME, $username);
$res = $this->userPasswordHasher->isPasswordValid(
$this->em->getRepository(User::class)->findOneBy(['email' => $username]),
$request->request->get('password', '')
);
if ($res){
return new SelfValidatingPassport(new UserBadge($username));
}else{
throw new BadCredentialsException('invalid email or password.');
}
}
//验证通过 逻辑,这里直接跳首页
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('front_index'));
}
//验证失败 逻辑
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
{
return parent::onAuthenticationFailure($request, $exception); // TODO: Change the autogenerated stub
}
protected function getLoginUrl(Request $request): string
{
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
}
}
SecurityController.php
<?php
namespace App\Controller\Front;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class SecurityController extends AbstractController
{
#[Route('/login', name: 'login_form', methods: ['get'])]
public function showLogin(Request $request): Response
{
//
if ($this->isGranted('IS_AUTHENTICATED_FULLY')) {
return $this->redirectToRoute('front_index');
} else {
return $this->render('security/index.html.twig');
}
}
#[Route('/login/success', name: 'login_success', methods: ['post'])]
public function loginSuccess(Request $request)
{
}
#[Route('/logout', name: 'logout')]
public function logOut(Request $request): void
{
}
}
登录form login.html.twig
{% extends 'base.html.twig' %}
{% block title %}Register{% endblock %}
{% block base_stylesheets %}
{{ encore_entry_link_tags('user_registers') }}
{% endblock %}
{% block body %}
<div class="admin-login-wrap">
<form class="admin-form-login" method="post" action="{{ path('login_success') }}" novalidate autocomplete="off">
<input type="hidden" name="_csrf_token"
value="{{ csrf_token('authenticate') }}"></input>
<div class="text-center mb-4">
<h1 class="mb-3 login-title">Login With Email </h1>
</div>
<div class="form-label-group">
<input type="text" id="inputEmail" name="email" class="form-control" placeholder="Email" required="" autofocus="" value="">
</div>
<div class="form-label-group">
<input type="password" id="inputPassword" name="password" class="form-control" placeholder="Password" required="" autofocus="" value="">
</div>
<button class="btn btn-lg btn-block" type="submit">Login</button>
</form>
</div>
{% endblock %}
{% block base_javascripts %}
{# {{ encore_entry_script_tags('user_registers') }}#}
{% endblock %}
