Automatyczne uwierzytelnianie użytkownika po rejestracji

114

Budujemy aplikację biznesową od podstaw w Symfony 2 i napotkałem pewien problem z przepływem rejestracji użytkownika: po utworzeniu konta przez użytkownika, powinien on zostać automatycznie zalogowany przy użyciu tych poświadczeń. natychmiastowego ponownego podania danych uwierzytelniających.

Czy ktoś miał z tym jakieś doświadczenie lub był w stanie wskazać mi właściwy kierunek?

Problematyczny
źródło

Odpowiedzi:

146

Symfony 4.0

Ten proces nie zmienił się z symfony 3 na 4, ale tutaj jest przykład używający nowo zalecanego AbstractController. Zarówno usługa, jak security.token_storagei sessionusługi są zarejestrowane w getSubscribedServicesmetodzie nadrzędnej , więc nie musisz ich dodawać w kontrolerze.

use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use YourNameSpace\UserBundle\Entity\User;

class LoginController extends AbstractController{

    public function registerAction()
    {    
        $user = //Handle getting or creating the user entity likely with a posted form
        $token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
        $this->container->get('security.token_storage')->setToken($token);
        $this->container->get('session')->set('_security_main', serialize($token));
        // The user is now logged in, you can redirect or do whatever.
    }

}

Symfony 2.6.x - Symfony 3.0.x

Od Symfony 2.6 security.contextjest przestarzałe na korzyść security.token_storage. Teraz kontroler może być po prostu:

use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use YourNameSpace\UserBundle\Entity\User;

class LoginController extends Controller{

    public function registerAction()
    {    
        $user = //Handle getting or creating the user entity likely with a posted form
        $token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
        $this->get('security.token_storage')->setToken($token);
        $this->get('session')->set('_security_main', serialize($token));
    }

}

Chociaż jest to przestarzałe, nadal możesz z niego korzystać, security.contextponieważ zostało utworzone jako zgodne z poprzednimi wersjami. Po prostu bądź gotowy, aby zaktualizować go do Symfony 3

Możesz przeczytać więcej o zmianach w 2.6 dla bezpieczeństwa tutaj: https://github.com/symfony/symfony/blob/2.6/UPGRADE-2.6.md

Symfony 2.3.x

Aby to osiągnąć w symfony 2.3, nie możesz już po prostu ustawić tokena w kontekście bezpieczeństwa. Musisz również zapisać token w sesji.

Zakładając plik bezpieczeństwa z zaporą ogniową, taką jak:

// app/config/security.yml
security:
    firewalls:
        main:
            //firewall settings here

A akcja kontrolera też jest podobna:

use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use YourNameSpace\UserBundle\Entity\User;

class LoginController extends Controller{

    public function registerAction()
    {    
        $user = //Handle getting or creating the user entity likely with a posted form
        $token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
        $this->get('security.context')->setToken($token);
        $this->get('session')->set('_security_main',serialize($token));
        //Now you can redirect where ever you need and the user will be logged in
    }

}

Aby utworzyć token, będziesz chciał utworzyć UsernamePasswordToken, To akceptuje 4 parametry: Jednostka użytkownika, Poświadczenia użytkownika, Nazwa zapory, Role użytkownika. Nie musisz podawać poświadczeń użytkownika, aby token był ważny.

Nie jestem w 100% pewien, czy ustawienie tokena na to security.contextjest konieczne, jeśli zamierzasz od razu przekierować. Ale to nie wydaje się boleć, więc zostawiłem to.

Następnie ważna część, ustawienie zmiennej sesji. Po konwencji nazewnictwa zmiennych _security_następuje nazwa zapory, w tym przypadku maintworzona_security_main

Pościg
źródło
1
Zaimplementowałem kod, użytkownik zalogował się pomyślnie, ale obiekt $ this-> getUser () zwraca NULL. Dowolny pomysł?
satish
2
Bez niej działy się szalone rzeczy $this->get('session')->set('_security_main', serialize($token));. Dziękuję @Chausser!
Dmitriy
1
W Symfony 2.6, jeśli ustawisz token dla firewalla o nazwie mainORAZ jesteś uwierzytelniony za pomocą innego firewalla o nazwie admin(jako że podszywasz się pod użytkownika), dzieje się dziwna rzecz: _security_admindostaje się UsernamePasswordTokenz podanym użytkownikiem, tzn. Zostajesz "rozłączony" Twój adminfirewall. Masz jakiś pomysł, jak zachować token dla zapory "administratora"?
gremo
1
Szczerze mówiąc, nie jestem pewien, czy możesz być uwierzytelniony dla 2 zapór ogniowych w tym samym czasie, źle się temu przyjrzę, ale w międzyczasie powinieneś zadać osobne pytanie
Chase
3
@Chausser sprawił, że to zadziałało. Twoja odpowiedź jest całkowicie poprawna (i jest aktualizowana), jedyne, co działa tylko wtedy, gdy dzwonisz setToken(..)pod tą samą docelową zaporą ogniową lub bez uwierzytelnienia .
gremo
65

W końcu to rozgryzłem.

Po rejestracji użytkownika powinieneś mieć dostęp do instancji obiektu, którą ustawiłeś jako encję użytkownika w konfiguracji dostawcy. Rozwiązaniem jest utworzenie nowego tokenu z tą jednostką użytkownika i przekazanie go do kontekstu zabezpieczeń. Oto przykład oparty na mojej konfiguracji:

RegistrationController.php:

$token = new UsernamePasswordToken($userEntity, null, 'main', array('ROLE_USER'));
$this->get('security.context')->setToken($token);

Gdzie mainjest nazwa zapory sieciowej dla Twojej aplikacji (dzięki, @Joe). To naprawdę wszystko; system traktuje teraz w pełni zalogowanego użytkownika jako użytkownika, którego właśnie utworzył.

EDYCJA: Zgodnie z komentarzem @ Miquel zaktualizowałem przykładowy kod kontrolera, aby uwzględnić rozsądną domyślną rolę dla nowego użytkownika (choć oczywiście można to dostosować do specyficznych potrzeb aplikacji).

Problematyczny
źródło
2
Nie jest to całkiem poprawne w wydaniu Symfony 2. Musisz przekazać role użytkownika jako czwarty argument do konstruktora UsernamePasswordToken, w przeciwnym razie zostanie on oznaczony jako nieuwierzytelniony i użytkownik nie będzie miał żadnej ze swoich ról.
Michael,
A co z flagą „Pamiętaj mnie”? Jak ręcznie logować użytkowników, ale też powinni być zalogowani na zawsze. Ten fragment kodu nie rozwiązuje tego problemu.
maectpo
@maectpo, które nie mieściło się w zakresie moich pierwotnych wymagań, ale brzmi jak świetna odpowiedź uzupełniająca. Daj nam znać, co wymyśliłeś.
Problematyczne
Mam problem. Mogę się zalogować w ten sposób, ale zmienna app.user jest pusta. Czy znasz jakiś sposób na wypełnienie tej zmiennej w tym procesie logowania? - Wysyłam
uniroldan
1
Jak powiedział Marc poniżej, musisz zarejestrować przestrzeń nazw UsernamePasswordToken:use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
MrGlass.
6

Jeśli masz obiekt UserInterface (i tak powinno być przez większość czasu), możesz chcieć użyć funkcji getRoles, którą implementuje dla ostatniego argumentu. Więc jeśli utworzysz funkcję logUser, powinno to wyglądać tak:

public function logUser(UserInterface $user) {
    $token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
    $this->container->get('security.context')->setToken($token);
}
Cédric Nirousset
źródło
6

Używam Symfony 2.2 i moje doświadczenia były nieco inne niż Problematic , więc jest to połączona wersja wszystkich informacji z tego pytania plus część moich własnych.

Myślę, że Joe myli się co do wartości $providerKeytrzeciego parametru UsernamePasswordTokenkonstruktora. Powinien to być klucz dostawcy uwierzytelniania (nie użytkownika). Jest używany przez system uwierzytelniania do rozróżniania tokenów utworzonych dla różnych dostawców. Każdy dostawca, który pochodzi od, UserAuthenticationProviderbędzie uwierzytelniał tylko tokeny, których klucz dostawcy jest zgodny z jego własnym. Na przykład UsernamePasswordFormAuthenticationListenerustawia klucz tokenu, który tworzy, tak, aby był zgodny z odpowiadającym mu kluczem DaoAuthenticationProvider. Dzięki temu pojedyncza zapora sieciowa może mieć wielu dostawców nazw użytkowników i haseł bez wzajemnego nadeptywania. Dlatego musimy wybrać klucz, który nie będzie kolidował z żadnym innym dostawcą. Używam 'new_user'.

Mam kilka systemów w innych częściach mojej aplikacji, które zależą od zdarzenia pomyślnego uwierzytelnienia i nie są uruchamiane przez samo ustawienie tokenu w kontekście. Musiałem wyjąć plik EventDispatcherz kontenera i ręcznie odpalić zdarzenie. Zdecydowałem się nie uruchamiać również interaktywnego zdarzenia logowania, ponieważ uwierzytelniamy użytkownika niejawnie, a nie w odpowiedzi na jawne żądanie logowania.

use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\Security\Core\Event\AuthenticationEvent;

$user = // get a Symfony user instance somehow
$token = new UsernamePasswordToken(
        $user, null, 'new_user', $user->getRoles() );
$this->get( 'security.context' )->setToken( $token );
$this->get( 'event_dispatcher' )->dispatch(
        AuthenticationEvents::AUTHENTICATION_SUCCESS,
        new AuthenticationEvent( $token ) );

Zauważ, że użycie of $this->get( .. )zakłada, że ​​fragment kodu jest metodą kontrolera. Jeśli używasz kodu w innym miejscu, będziesz musiał zmienić je, aby wywoływały ContainerInterface::get( ... )w sposób odpowiedni dla środowiska. Tak się składa, że ​​moje encje użytkowników są implementowane, UserInterfacewięc mogę ich używać bezpośrednio z tokenem. Jeśli nie masz, musisz znaleźć sposób na ich konwersjęUserInterface instancje.

Ten kod działa, ale wydaje mi się, że zamiast pracować z nią, hackuje architekturę uwierzytelniania Symfony. Prawdopodobnie bardziej poprawne byłoby zaimplementowanie nowego dostawcy uwierzytelniania z własną klasą tokenów, zamiast przechwytywania UsernamePasswordToken. Ponadto użycie odpowiedniego dostawcy oznaczałoby, że zdarzenia byłyby obsługiwane za Ciebie.

Sam Hanes
źródło
4

Na wypadek, gdyby ktoś miał to samo pytanie, do którego wracałem tutaj:

Powołanie

$this->container->get('security.context')->setToken($token); 

wpływa tylko na prąd security.contextdla używanej trasy.

Oznacza to, że możesz zalogować użytkownika tylko z adresu URL znajdującego się pod kontrolą zapory.

(W razie potrzeby dodaj wyjątek dla trasy - IS_AUTHENTICATED_ANONYMOUSLY)

daemonl
źródło
1
czy wiesz, jak to robisz na sesję? Zamiast tylko do aktualnego żądania?
Jake N
3

W Symfony 4.4 możesz po prostu wykonać następujące czynności w swojej metodzie kontrolera (patrz w dokumentacji Symfony: https://symfony.com/doc/current/security/guard_authentication.html#manually-authenticating-a-user ):

// src/Controller/RegistrationController.php
// ...

use App\Security\LoginFormAuthenticator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;

class RegistrationController extends AbstractController
{
    public function register(LoginFormAuthenticator $authenticator, GuardAuthenticatorHandler $guardHandler, Request $request)
    {
        // ...

        // after validating the user and saving them to the database
        // authenticate the user and use onAuthenticationSuccess on the authenticator
        return $guardHandler->authenticateUserAndHandleSuccess(
            $user,          // the User object you just created
            $request,
            $authenticator, // authenticator whose onAuthenticationSuccess you want to use
            'main'          // the name of your firewall in security.yaml
        );
    }
}

Jedna ważna rzecz, upewnij się, że twoja zapora nie jest ustawiona na lazy. Jeśli tak, token nigdy nie zostanie zapisany w sesji i nigdy nie zostaniesz zalogowany.

firewalls:
    main:
        anonymous: ~ # this and not 'lazy'
Etienne Noël
źródło
2

Jak już wspomniałem Problematic, ten nieuchwytny parametr $ providerKey jest w rzeczywistości niczym innym jak nazwą twojej reguły firewalla, „foobar” w przypadku poniższego przykładu.

firewalls:
    foobar:
        pattern:    /foo/
Nim
źródło
Czy możesz mi wyjaśnić, dlaczego jeśli przekażę dowolny ciąg, na przykład blablablajako trzeci parametr do UsernamePasswordToken, to też zadziała? co oznacza ten parametr?
Michaił
1
Ten parametr wiąże twój token z określonym dostawcą zapory. W większości przypadków będziesz mieć tylko jednego dostawcę, więc nie przejmuj się tym.
Gottlieb Notschnabel
2

Wypróbowałem wszystkie odpowiedzi tutaj i żadna nie działała. Jedynym sposobem, w jaki mogę uwierzytelnić moich użytkowników na kontrolerze, jest wysłanie żądania dodatkowego, a następnie przekierowanie. Oto mój kod, używam silex, ale możesz łatwo dostosować go do symfony2:

$subRequest = Request::create($app['url_generator']->generate('login_check'), 'POST', array('_username' => $email, '_password' => $password, $request->cookies->all(), array(), $request->server->all());

$response = $app->handle($subRequest, HttpKernelInterface::MASTER_REQUEST, false);

return $app->redirect($app['url_generator']->generate('curriculos.editar'));
Diego Castro
źródło
1

Na Symfony w wersji 2.8.11 (prawdopodobnie działa dla starszych i nowszych wersji), jeśli używasz FOSUserBundle, po prostu zrób to:

try {
    $this->container->get('fos_user.security.login_manager')->loginUser(
    $this->container->getParameter('fos_user.firewall_name'), $user, null);
} catch (AccountStatusException $ex) {
    // We simply do not authenticate users which do not pass the user
    // checker (not enabled, expired, etc.).
}

Nie ma potrzeby wysyłania zdarzenia, jak widziałem w innych rozwiązaniach.

zainspirowany z FOS \ UserBundle \ Controller \ RegistrationController :: autenticateUser

(z wersji composer.json FOSUserBundle: „friendsofsymfony / user-bundle”: „~ 1.3”)

Nico
źródło