注册后自动用户身份验证


Automatic post-registration user authentication

我们正在Symfony 2中从头开始构建一个商业应用程序,我在用户注册流程中遇到了一些障碍:用户创建帐户后,他们应该使用这些凭据自动登录,而不是立即被迫再次提供凭据。

有人对此有任何经验,或者能够为我指出正确的方向吗?

Symfony 4.0

这个过程从Symfony 3到4没有改变,但这里有一个使用新推荐AbstractController的例子。security.token_storagesession 服务都在父 getSubscribedServices 方法中注册,因此无需在控制器中添加它们。

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

从Symfony 2.6开始,security.context被弃用,取而代之的是security.token_storage。控制器现在可以简单地:

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

虽然这已被弃用,但您仍然可以使用 security.context,因为它已被设置为向后兼容。准备好为Symfony 3更新它。

您可以在此处阅读有关 2.6 安全更改的更多信息:https://github.com/symfony/symfony/blob/2.6/UPGRADE-2.6.md

Symfony 2.3.x

要在Symfony 2.3中完成此操作,您不能再只在安全上下文中设置令牌。您还需要将令牌保存到会话。

假设带有防火墙的安全文件,如下所示:

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

以及类似于以下内容的控制器操作:

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

对于令牌创建,您需要创建一个UsernamePasswordToken。这接受 4 个参数:用户实体、用户凭据、防火墙名称、用户角色。无需提供用户凭据即可使令牌有效。

如果您只是要立即重定向,我不是 100% 确定在security.context上设置令牌是必要的。但它似乎不疼,所以我离开了它。

然后是重要的部分,设置会话变量。变量命名约定_security_后跟您的防火墙名称,在本例中main进行_security_main

终于

想通了。

用户

注册后,您应该有权访问您在提供程序配置中设置为用户实体的任何对象实例。解决方案是使用该用户实体创建一个新令牌,并将其传递到安全上下文中。下面是一个基于我的设置的示例:

RegistrationController.php:

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

其中main是应用程序的防火墙名称(谢谢,@Joe)。这就是它的全部内容;系统现在会将您的用户视为他们刚刚创建的用户完全登录。

编辑:根据@Miquel的评论,我已经更新了控制器代码示例,为新用户提供了一个合理的默认角色(尽管显然可以根据应用程序的特定需求进行调整)。

如果你有一个用户界面对象(大多数时候应该是这种情况),你可能希望使用它为最后一个参数实现的 getRoles 函数。因此,如果您创建一个函数 logUser,它应该如下所示:

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

我使用的是Symfony 2.2,我的体验与Problem的略有不同,所以这是这个问题中所有信息以及我自己的一些信息的组合版本。

我认为 Joe 对 $providerKey 的值是错误的,UsernamePasswordToken构造函数的第三个参数。它应该是身份验证(而不是用户)提供程序的密钥。身份验证系统使用它来区分为不同提供程序创建的令牌。 任何从 UserAuthenticationProvider 衍生的提供程序将仅对提供程序密钥与其自身密钥匹配的令牌进行身份验证。例如,UsernamePasswordFormAuthenticationListener 设置它创建的令牌的密钥,以匹配其相应DaoAuthenticationProvider的密钥。这使得单个防火墙具有多个用户名+密码提供程序,而无需它们相互踩踏。因此,我们需要选择一个不会与任何其他提供程序冲突的密钥。我使用'new_user'.

我在应用程序的其他部分中有一些依赖于身份验证成功事件的系统,并且不是通过在上下文中设置令牌来触发的。我必须从容器中获取EventDispatcher并手动触发事件。我决定不同时触发交互式登录事件,因为我们对用户进行隐式身份验证,而不是响应显式登录请求。

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

请注意,使用 $this->get( .. ) 假定代码段位于控制器方法中。如果您在其他地方使用代码,则必须更改这些代码以适合环境的方式调用ContainerInterface::get( ... )。碰巧我的用户实体实现了UserInterface因此我可以直接将它们与令牌一起使用。如果没有,则必须找到将它们转换为UserInterface实例的方法。

该代码有效,但我觉得它正在破解Symfony的身份验证架构,而不是使用它。使用自己的令牌类实现新的身份验证提供程序可能更正确,而不是劫持UsernamePasswordToken。此外,使用适当的提供程序意味着已为您处理事件。

使用Symfony

4.4,您可以在控制器方法中简单地执行以下操作(请参阅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
        );
    }
}

一件重要的事情,确保您的防火墙未设置为 lazy .如果是,令牌将永远不会存储在会话中,您将永远不会登录。

firewalls:
    main:
        anonymous: ~ # this and not 'lazy'

如果有人有同样的后续问题,让我回到这里:

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

仅影响所用路由的当前security.context

即您只能从防火墙控制范围内的 url 登录用户。

(如果需要,为路由添加例外 - IS_AUTHENTICATED_ANONYMOUSLY

正如这里已经提到的问题,这个难以捉摸的$providerKey参数实际上只不过是防火墙规则的名称,在下面的示例中为"foobar"。

firewalls:
    foobar:
        pattern:    /foo/

我在这里尝试了所有答案,但没有一个有效。我可以在控制器上对用户进行身份验证的唯一方法是发出子请求,然后重定向。这是我的代码,我正在使用silex,但你可以很容易地将其适应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'));

在Symfony版本2.8.11(可能适用于旧版本和新版本)上,如果您使用FOSUserBundle,只需执行以下操作:

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

无需像我在其他解决方案中看到的那样调度事件。

从 FOS''UserBundle''Controller''RegistrationController::authenticateUser

(来自 composer.json FOSUserBundle 版本:"friendsofsymfony/user-bundle":"~1.3")