Autenticación automática de usuario posterior al registro

114

Estamos creando una aplicación empresarial desde cero en Symfony 2, y me he encontrado con un pequeño inconveniente con el flujo de registro del usuario: después de que el usuario crea una cuenta, debería iniciar sesión automáticamente con esas credenciales, en su lugar de verse obligado inmediatamente a proporcionar sus credenciales nuevamente.

¿Alguien ha tenido alguna experiencia con esto o ha sido capaz de indicarme la dirección correcta?

Problemático
fuente

Respuestas:

146

Symfony 4.0

Este proceso no ha cambiado de Symfony 3 a 4, pero aquí hay un ejemplo utilizando el AbstractController recomendado recientemente. Tanto las security.token_storagey los sessionservicios están registrados en la matriz getSubscribedServicesmétodo para que no tenga que añadir los de su controlador.

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

A partir de Symfony 2.6 security.contextestá obsoleto en favor de security.token_storage. El controlador ahora puede ser simplemente:

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));
    }

}

Si bien esto está obsoleto, aún puede usarlo, security.contextya que se ha hecho para ser compatible con versiones anteriores. Solo prepárate para actualizarlo para Symfony 3

Puede leer más sobre los cambios 2.6 por seguridad aquí: https://github.com/symfony/symfony/blob/2.6/UPGRADE-2.6.md

Symfony 2.3.x

Para lograr esto en Symfony 2.3, ya no puedes simplemente configurar el token en el contexto de seguridad. También debe guardar el token en la sesión.

Suponiendo un archivo de seguridad con un firewall como:

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

Y una acción de controlador similar también:

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
    }

}

Para la creación del token, querrá crear un UsernamePasswordToken, Esto acepta 4 parámetros: Entidad de usuario, Credenciales de usuario, Nombre del firewall, Roles de usuario. No es necesario que proporcione las credenciales de usuario para que el token sea válido.

No estoy 100% seguro de que security.contextsea ​​necesario configurar el token en el si solo va a redireccionar de inmediato. Pero no parece doler, así que lo dejé.

Luego, la parte importante, configurar la variable de sesión. La convención de nomenclatura de variables es _security_seguida por el nombre de su firewall, en este caso mainhaciendo_security_main

Persecución
fuente
1
Implementé el código, el usuario inició sesión correctamente, pero el objeto $ this-> getUser () devuelve NULL. ¿Alguna idea?
Satish
2
Fuera estaban sucediendo cosas locas $this->get('session')->set('_security_main', serialize($token));. ¡Gracias, @Chausser!
Dmitriy
1
Con Symfony 2.6, si configuras el token para un cortafuegos llamado mainY estás autenticado con otro cortafuegos llamado admin(ya que te estás haciendo pasar por el usuario), sucede algo extraño: _security_adminobtiene el UsernamePasswordTokencon el usuario que proporcionaste, es decir, te "desconecta" de su adminfirewall. ¿Alguna idea de cómo mantener el token para el firewall "admin"?
gremo
1
Para ser honesto, no estoy seguro de que pueda ser autenticado para 2 firewalls al mismo tiempo, lo investigaré, pero mientras tanto, debe hacer una pregunta separada
Chase
3
@Chausser logró que funcionara. Su respuesta es perfectamente correcta (y está actualizada), lo único que funciona solo cuando llama setToken(..)bajo el mismo firewall de destino o sin estar autenticado aún.
Gremo
65

Descubrí este, finalmente.

Después del registro de usuario, debe tener acceso a una instancia de objeto de lo que haya establecido como su entidad de usuario en la configuración de su proveedor. La solución es crear un nuevo token con esa entidad de usuario y pasarlo al contexto de seguridad. Aquí hay un ejemplo basado en mi configuración:

RegistrationController.php:

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

¿Dónde mainestá el nombre del firewall para su aplicación (gracias, @Joe). Eso es realmente todo lo que hay que hacer; el sistema ahora considera que su usuario ha iniciado sesión como el usuario que acaba de crear.

EDITAR: Según el comentario de @ Miquel, he actualizado la muestra de código del controlador para incluir un rol predeterminado sensible para un nuevo usuario (aunque obviamente esto se puede ajustar de acuerdo con las necesidades específicas de su aplicación).

Problemático
fuente
2
Esto no es del todo correcto con la versión de lanzamiento de Symfony 2. Necesitas pasar los roles del usuario como un cuarto argumento al constructor UsernamePasswordToken, o se marcará como no autenticado y el usuario no tendrá ninguno de sus roles.
Michael
¿Qué pasa con la bandera "Recuérdame"? Cómo iniciar sesión a los usuarios a mano, pero también deben iniciar sesión para siempre. Este fragmento de código no resuelve ese problema.
maectpo
@maectpo que no estaba en el alcance de mis requisitos originales, pero suena como una gran respuesta de seguimiento. Háganos saber lo que se le ocurrió.
Problemática
Tengo un problema Puedo iniciar sesión de esta manera, pero la variable app.user está vacía. ¿Conoce alguna forma de completar esta variable en este proceso de inicio de sesión? - Envío el usuario (cadena) y contraseña (cadena) como dice la Referencia: api.symfony.com/2.0/Symfony/Component/Security/Core/…
unairoldan
1
Como dijo Marc a continuación, debe registrar el espacio de nombres UsernamePasswordToken:use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
MrGlass
6

Si tiene un objeto UserInterface (y ese debería ser el caso la mayor parte del tiempo), es posible que desee utilizar la función getRoles que implementa para el último argumento. Entonces, si crea una función logUser, debería verse así:

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

Estoy usando Symfony 2.2 y mi experiencia fue ligeramente diferente a la de Problematic , por lo que esta es una versión combinada de toda la información de esta pregunta más parte de la mía.

Creo que Joe está equivocado sobre el valor de $providerKey, el tercer parámetro del UsernamePasswordTokenconstructor. Se supone que es la clave de un proveedor de autenticación (no de un usuario). Es utilizado por el sistema de autenticación para distinguir entre tokens creados para diferentes proveedores. Cualquier proveedor que descienda de UserAuthenticationProvidersolo autenticará tokens cuya clave de proveedor coincida con la suya. Por ejemplo, UsernamePasswordFormAuthenticationListenerestablece la clave del token que crea para que coincida con la de su correspondiente DaoAuthenticationProvider. Eso permite que un solo firewall tenga múltiples proveedores de nombre de usuario + contraseña sin que se pisen entre sí. Por lo tanto, debemos elegir una clave que no entre en conflicto con ningún otro proveedor. yo suelo'new_user' .

Tengo algunos sistemas en otras partes de mi aplicación que dependen del evento de éxito de la autenticación , y eso no se activa simplemente configurando el token en el contexto. Tuve que sacar el EventDispatcherdel contenedor y disparar el evento manualmente. Decidí no activar también un evento de inicio de sesión interactivo porque estamos autenticando al usuario implícitamente, no en respuesta a una solicitud de inicio de sesión explícita.

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 ) );

Tenga en cuenta que el uso de $this->get( .. )asume que el fragmento está en un método de controlador. Si está utilizando el código en otro lugar, tendrá que cambiarlos para llamar ContainerInterface::get( ... )de una manera adecuada al entorno. Como sucede, mis entidades de usuario implementan UserInterfacepara que pueda usarlas directamente con el token. Si el tuyo no es así, tendrás que encontrar una manera de convertirlos aUserInterface instancias.

Ese código funciona, pero siento que está pirateando la arquitectura de autenticación de Symfony en lugar de trabajar con ella. Probablemente sería más correcto implementar un nuevo proveedor de autenticación con su propia clase de token en lugar de secuestrar el UsernamePasswordToken. Además, usar un proveedor adecuado significaría que los eventos se manejaron por usted.

Sam Hanes
fuente
4

En caso de que alguien tenga la misma pregunta de seguimiento que me hizo volver aquí:

Vocación

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

solo afecta la corriente security.context de la ruta utilizada.

Es decir, solo puede iniciar sesión un usuario desde una URL dentro del control del firewall.

(Agregue una excepción para la ruta si es necesario - IS_AUTHENTICATED_ANONYMOUSLY)

demonio
fuente
1
¿Sabes cómo haces esto para una sesión? ¿En lugar de solo por la solicitud actual?
Jake N
3

Con Symfony 4.4, simplemente puede hacer lo siguiente en su método de controlador (consulte la documentación de 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
        );
    }
}

Una cosa importante, asegúrese de que su firewall no esté configurado en lazy. Si es así, el token nunca se almacenará en la sesión y nunca iniciará sesión.

firewalls:
    main:
        anonymous: ~ # this and not 'lazy'
Etienne Noël
fuente
2

Como Problematic ya mencionó aquí, este elusivo parámetro $ providerKey no es en realidad más que el nombre de su regla de firewall, 'foobar' en el caso del ejemplo siguiente.

firewalls:
    foobar:
        pattern:    /foo/
Nim
fuente
¿Puede explicarme por qué si paso la cadena any, por ejemplo, blablablacomo tercer parámetro a UsernamePasswordToken, también funcionará? ¿Qué significa este parámetro?
Mikhail
1
Este parámetro vincula su token a un proveedor de firewall específico. En la mayoría de los casos, solo tendrá un proveedor, así que no se preocupe.
Gottlieb Notschnabel
2

Probé todas las respuestas aquí y ninguna funcionó. La única forma en que podría autenticar a mis usuarios en un controlador es haciendo una subconsulta y luego redireccionando. Aquí está mi código, estoy usando silex pero puedes adaptarlo fácilmente a 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
fuente
1

En Symfony versión 2.8.11 (probablemente funciona para versiones más antiguas y más nuevas), si usa FOSUserBundle simplemente haga esto:

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.).
}

No es necesario enviar eventos como he visto en otras soluciones.

inspirado en FOS \ UserBundle \ Controller \ RegistrationController :: authenticateUser

(de composer.json FOSUserBundle versión: "friendsofsymfony / user-bundle": "~ 1.3")

Nico
fuente