Autenticação automática de usuário pós-registro

114

Estamos construindo um aplicativo de negócios a partir do zero no Symfony 2, e me deparei com um pequeno obstáculo com o fluxo de registro do usuário: depois que o usuário cria uma conta, ele deve ser automaticamente conectado com essas credenciais, em vez de ser imediatamente forçado a fornecer suas credenciais novamente.

Alguém já teve alguma experiência com isso, ou é capaz de me apontar na direção certa?

Problemático
fonte

Respostas:

146

Symfony 4.0

Este processo não mudou do symfony 3 para 4, mas aqui está um exemplo usando o recém-recomendado AbstractController. Os serviços security.token_storagee os sessionsão registrados no getSubscribedServicesmétodo pai , portanto, você não precisa adicioná-los ao seu 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 do symfony 2.6 security.contextestá obsoleto em favor de security.token_storage. O controlador agora pode ser simplesmente:

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

}

Embora esteja obsoleto, você ainda pode usar security.context, pois foi feito para ser compatível com versões anteriores. Esteja pronto para atualizá-lo para Symfony 3

Você pode ler mais sobre as mudanças 2.6 para segurança aqui: https://github.com/symfony/symfony/blob/2.6/UPGRADE-2.6.md

Symfony 2.3.x

Para conseguir isso no symfony 2.3, você não pode mais apenas definir o token no contexto de segurança. Você também precisa salvar o token na sessão.

Presumindo um arquivo de segurança com um firewall como:

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

E uma ação de controle semelhante também:

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 a criação do token, você desejará criar um UsernamePasswordToken, que aceita 4 parâmetros: Entidade do usuário, Credenciais do usuário, Nome do firewall, Funções do usuário. Você não precisa fornecer as credenciais do usuário para que o token seja válido.

Não tenho 100% de certeza de que definir o token no security.contextseja necessário se você for redirecionar imediatamente. Mas não parece doer, então eu o deixei.

Em seguida, a parte importante, definir a variável de sessão. A convenção variáveis nomeando é _security_seguido pelo seu nome firewall, neste caso, maina tomada_security_main

correr atrás
fonte
1
Eu implementei o código, Usuário conectado com sucesso, mas o objeto $ this-> getUser () retorna NULL. Qualquer ideia?
sathish
2
Coisas malucas estavam acontecendo lá fora $this->get('session')->set('_security_main', serialize($token));. Obrigado, @Chausser!
Dmitriy
1
Com Symfony 2.6 se você definir o token para um firewall chamado mainE você for autenticado com outro firewall chamado admin(já que você está se passando por usuário), uma coisa estranha acontece: o _security_adminobtém o UsernamePasswordTokencom o usuário fornecido, ou seja, você é "desconectado" do seu adminfirewall. Alguma ideia de como manter o token para o firewall "admin"?
gremo
1
Para ser honesto, não tenho certeza se você pode ser autenticado por 2 firewalls ao mesmo tempo, vou dar uma olhada nisso, mas nesse meio tempo você deve fazer uma pergunta separada
Chase
3
@Chausser conseguiu fazer funcionar. A sua resposta está perfeitamente certa (e está atualizada), a única coisa que funciona apenas quando você liga setToken(..)no mesmo firewall de destino ou sem estar autenticado ainda.
gremo
65

Descobri este, finalmente.

Após o registro do usuário, você deve ter acesso a uma instância de objeto de tudo o que você definiu como sua entidade de usuário na configuração do seu provedor. A solução é criar um novo token com essa entidade de usuário e passá-lo para o contexto de segurança. Aqui está um exemplo baseado em minha configuração:

RegistrationController.php:

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

Onde main está o nome do firewall do seu aplicativo (obrigado, @Joe). Isso é realmente tudo que há para fazer; o sistema agora considera seu usuário totalmente logado como o usuário que acabou de criar.

EDIT: Com o comentário de @Miquel, atualizei o exemplo de código do controlador para incluir uma função padrão sensata para um novo usuário (embora, obviamente, isso possa ser ajustado de acordo com as necessidades específicas do seu aplicativo).

Problemático
fonte
2
Isso não está certo com a versão de lançamento do Symfony 2. Você precisa passar as funções do usuário como um quarto argumento para o construtor UsernamePasswordToken, ou será marcado como não autenticado e o usuário não terá nenhuma de suas funções.
Michael,
E a bandeira "Lembre-se de mim"? Como fazer o login dos usuários manualmente, mas também eles devem estar logados para sempre. Este pedaço de código não resolve esse problema.
maectpo
@maectpo que não estava no escopo dos meus requisitos originais, mas parece uma ótima resposta complementar. Deixe-nos saber o que você descobriu.
Problemático
Eu tenho um problema. Posso logar dessa forma, mas a variável app.user está vazia. Você conhece alguma maneira de preencher esta variável neste processo de login? - Eu envio o usuário (string) e a senha (string) como diz a Referência: api.symfony.com/2.0/Symfony/Component/Security/Core/…
unairoldan
1
Como Marc disse abaixo, você precisa registrar o namespace UsernamePasswordToken:use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
MrGlass
6

Se você tiver um objeto UserInterface (e esse deve ser o caso na maioria das vezes), você pode querer usar a função getRoles que ele implementa para o último argumento. Portanto, se você criar uma função logUser, ela deve se parecer com:

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

Estou usando Symfony 2.2 e minha experiência foi um pouco diferente da da Problemática , então esta é uma versão combinada de todas as informações desta questão mais algumas das minhas.

Acho que Joe está errado sobre o valor de $providerKey, o terceiro parâmetro do UsernamePasswordTokenconstrutor. Supõe-se que seja a chave de um provedor de autenticação (não do usuário). É usado pelo sistema de autenticação para distinguir entre tokens criados para diferentes provedores. Qualquer provedor descendente de UserAuthenticationProviderautenticará apenas tokens cuja chave de provedor corresponda à sua. Por exemplo, o UsernamePasswordFormAuthenticationListenerdefine a chave do token que ele cria para corresponder ao seu correspondente DaoAuthenticationProvider. Isso permite que um único firewall tenha vários provedores de nome de usuário + senha sem que eles se atropelem. Portanto, precisamos escolher uma chave que não entre em conflito com nenhum outro provedor. Eu uso 'new_user'.

Tenho alguns sistemas em outras partes do meu aplicativo que dependem do evento de sucesso da autenticação e que não são acionados apenas definindo o token no contexto. Tive que obter o EventDispatcherdo contêiner e disparar o evento manualmente. Decidi também não disparar um evento de login interativo porque estamos autenticando o usuário implicitamente, não em resposta a uma solicitação de login 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 ) );

Observe que o uso de $this->get( .. )assume que o snippet está em um método de controlador. Se estiver usando o código em outro lugar, você terá que alterá-lo para chamar ContainerInterface::get( ... )de uma forma apropriada ao ambiente. Acontece que minhas entidades de usuário implementam UserInterfacepara que eu possa usá-las diretamente com o token. Caso contrário, você terá que encontrar uma maneira de convertê-los paraUserInterface instâncias.

Esse código funciona, mas eu sinto que ele está hackeando a arquitetura de autenticação do Symfony ao invés de trabalhar com ela. Provavelmente seria mais correto implementar um novo provedor de autenticação com sua própria classe de token em vez de sequestrar o UsernamePasswordToken. Além disso, usar um provedor adequado significaria que os eventos foram gerenciados para você.

Sam Hanes
fonte
4

Caso alguém tenha a mesma pergunta que me fez voltar a aqui:

Chamando

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

afeta apenas a corrente security.contextpara a rota usada.

Ou seja, você só pode fazer login de um usuário a partir de uma url sob o controle do firewall.

(Adicione uma exceção para a rota, se necessário - IS_AUTHENTICATED_ANONYMOUSLY)

demônio
fonte
1
você sabe como fazer isso para uma sessão? Em vez de apenas pela solicitação atual?
Jake N
3

Com Symfony 4.4, você pode simplesmente fazer o seguinte em seu método de controlador (veja na documentação do 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
        );
    }
}

Uma coisa importante, certifique-se de que seu firewall não esteja configurado para lazy. Se for, o token nunca será armazenado na sessão e você nunca fará login.

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

Como Problemático já mencionado aqui, este parâmetro $ providerKey indescritível nada mais é do que o nome de sua regra de firewall, 'foobar' no caso do exemplo abaixo.

firewalls:
    foobar:
        pattern:    /foo/
Nim
fonte
Você pode me explicar por que, se eu passar qualquer string, por exemplo, blablablacomo terceiro parâmetro para UsernamePasswordToken, também funcionará? o que esse parâmetro significa?
Mikhail
1
Este parâmetro liga seu token a um provedor de firewall específico. Na maioria dos casos, você terá apenas um provedor, portanto, não se preocupe com isso.
Gottlieb Notschnabel
2

Tentei todas as respostas aqui e nenhuma funcionou. A única maneira de autenticar meus usuários em um controlador é fazendo uma sub-solicitação e redirecionando. Aqui está meu código, estou usando silex, mas você pode adaptá-lo facilmente para o 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
fonte
1

No Symfony versão 2.8.11 (provavelmente funcionando para versões mais antigas e mais recentes), se você usar FOSUserBundle, simplesmente faça o seguinte:

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

Não há necessidade de despachar o evento, como já vi em outras soluções.

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

(da versão composer.json FOSUserBundle: "friendsofsymfony / user-bundle": "~ 1,3")

Nico
fonte