WEBcoast Logo

Symfony: Login-Formular auf jeder Seite

In einem Symfony basierten Projekt, sollte es möglich sein, sich von allen Seiten aus direkt anzumelden und dann natürlich nach erfolgreicher Anmeldung direkt wieder auf die ursprüngliche Seite zurück zu kommen. In der Symfony-Dokumentation habe dazu direkt nichts gefunden. Allerdings habe ich verschiedene Events rund um die Authentifizierung gefunden sowie die Möglichkeit die Ziel-URL nach der erfolgreichen Anmeldung zu ändern. Das habe ich zu folgender Lösung kombiniert.

Auszug aus `security.yaml`:

security:
   ...
   firewalls:
       ...
       main:
          ...
          form_login:
             login_path: login
             check_path: login
             enable_csrf: true

Das Template für das Login-Formular enthält sowohl ein CSRF-Token als auch das Feld `_target_path`. Das Feld `_target_path` enthält folgende Werte, je nach Verfügbarkeit:

  • `targetPath` Variable aus dem `SecurityController` von einem vorherigen Anmeldeversuch
  • die aktuelle URL (aktuelle Route mit entsprechenden Parameters)
  • URL zur Startseite
<form method="post" class="form" action="{{ path('login') }}">
    <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
    <input type="hidden" name="_target_path" value="{{ targetPath | default(path(app.current_route, app.current_route_parameters)) | default('home') }}">
    ...
</form>

Ich habe einen Event Listener für das `LoginFailureEvent` registriert, der das Feld `_target_path` ausliest und in der Session speichert, damit dieses bei der erneuten Anzeige dem Formular zur Verfügung steht.

<?php

namespace MyVendor\MyPackage\EventListener;

use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Security\Http\Event\LoginFailureEvent;

#[AsEventListener]
class SaveTargetUrlOnLoginFailureListener
{
    public function __construct(protected Security $security)
    {
    }

    public function __invoke(LoginFailureEvent $event)
    {
        $request = $event->getRequest();
        $session = $request->getSession();
        $firewallName = $this->security->getFirewallConfig($request)->getName();
        $session->set('_security.' . $firewallName . '.target_path', $request->get('_target_path'));
    }
}

Schlussendlich noch der `SecurityController`, der für die Anzeige der Login-Seite zuständig ist. Dieser nutzt das `TargetPathTrait` und liest damit den gespeicherten `_target_path` aus der Session und stellt diesen dem Template für die erneute Anzeige zur Verfügung.

<?php

namespace MyVendor\MyPackage\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\Security\Http\Util\TargetPathTrait;

class SecurityController extends AbstractController
{
    use TargetPathTrait;

    #[Route(path: '/login', name: 'login')]
    public function login(Request $request, Session $session, AuthenticationUtils $authenticationUtils, Security $security): Response
    {
        // 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,
            'targetPath' => $this->getTargetPath($session, $security->getFirewallConfig($request)->getName()),
        ]);
    }
    
    // ...
}

Das Template für das Login-Formular habe dann noch in ein extra Twig-Template extrahiert, um diese per `include` überall einbinden zu können.

Ich hoffe, das Beispiel hilft dir dabei, dein Problem zu lösen. Lass gerne einen Kommentar da, wenn es dir geholfen hat oder falls du Fragen und/oder Anmerkungen hast.