指尖上的记忆指尖上的记忆
首页
  • 基础
  • Laravel框架
  • Symfony框架
  • 基础
  • Gin框架
  • 基础
  • Spring框架
  • 命令
  • Nginx
  • Ai
  • Deploy
  • Docker
  • K8s
  • Micro
  • RabbitMQ
  • Mysql
  • PostgreSsql
  • Redis
  • MongoDb
  • Html
  • Js
  • 前端
  • 后端
  • Git
  • 知识扫盲
  • Golang
🌟 gitHub
首页
  • 基础
  • Laravel框架
  • Symfony框架
  • 基础
  • Gin框架
  • 基础
  • Spring框架
  • 命令
  • Nginx
  • Ai
  • Deploy
  • Docker
  • K8s
  • Micro
  • RabbitMQ
  • Mysql
  • PostgreSsql
  • Redis
  • MongoDb
  • Html
  • Js
  • 前端
  • 后端
  • Git
  • 知识扫盲
  • Golang
🌟 gitHub

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 %}