自动注册后用户身份验证

117
我们正在使用Symfony 2从头开始构建一个业务应用程序,但在用户注册流程中遇到了一些问题:用户创建账户后,应自动使用这些凭据进行登录,而不是立即被要求再次提供凭据。
有没有人有相关经验或者能够指点我正确的方向?
9个回答

147

Symfony 4.0

在Symfony 3到4的过程中,这个过程并没有改变,但是下面的例子使用了新推荐的AbstractController。在父类的getSubscribedServices方法中注册了security.token_storagesession服务,因此您不需要在控制器中添加这些服务。

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


1
我已经实现了代码,用户成功登录,但$this->getUser()对象返回NULL。有什么想法吗? - sathish
2
没有 $this->get('session')->set('_security_main', serialize($token));,一些疯狂的事情正在发生。谢谢,@Chausser! - Dmytro
1
在Symfony 2.6中,如果您为名为“main”的防火墙设置令牌,并且使用名为“admin”的另一个防火墙进行身份验证(因为您正在模拟用户),则会发生奇怪的事情:_security_admin将获得您提供的用户的UsernamePasswordToken,即您将从admin防火墙中“断开连接”。有什么办法可以维护“admin”防火墙的令牌? - gremo
1
老实说,我不确定您是否可以同时通过两个防火墙进行身份验证,我会调查一下,但与此同时,您应该提出一个单独的问题。 - Chase
3
@Chausser成功让它工作了。你的答案完全正确(并已更新),唯一的问题是它只在你在相同的目标防火墙下调用setToken(..)或者尚未通过身份验证时才能正常工作。 - gremo
显示剩余13条评论

65

终于想通了。

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

RegistrationController.php:

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

这里的main是你的应用程序防火墙的名称(感谢@Joe)。就是这样,系统现在将您的用户视为刚创建的用户并完全登录。

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


2
这在Symfony 2的发布版本中不太正确。您需要将用户角色作为第四个参数传递给UsernamePasswordToken构造函数,否则它将被标记为未经身份验证,并且用户将无法使用其任何角色。 - Michael
1
“记住我”标志怎么办?如何手动登录用户,但他们也应该永久登录。这段代码无法解决这个问题。 - maectpo
1
就像Marc在下面所说的那样,您需要注册UsernamePasswordToken命名空间:use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; - MrGlass
这在2.1版本中能用吗?我正在使用您的代码,但没有用户登录。 - DomingoSL
顺便提一下,在2.6中可以使用 $this->container->get('security.authorization_checker') 代替。 - Ronan
显示剩余4条评论

6
如果您有一个UserInterface对象(大多数情况下应该是这样),您可能希望使用它实现的getRoles函数作为最后一个参数。
因此,如果您创建一个名为logUser的函数,它应该如下所示:
public function logUser(UserInterface $user) {
    $token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
    $this->container->get('security.context')->setToken($token);
}

6

我正在使用Symfony 2.2,我的经验与Problematic略有不同,因此这是一个结合了这个问题所有信息和我的一些信息的版本。

我认为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,则必须找到一种将它们转换为UserInterface实例的方法。
那段代码虽然可行,但我觉得它是在绕开Symfony的身份验证架构而不是与之配合。最好实现一个新的身份验证提供程序,带有自己的令牌类,而不是劫持UsernamePasswordToken。此外,使用正确的提供程序意味着事件将为您处理。

6
在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'

在Symfony 5.0中,我使它工作的唯一方法是,没有破损的会话和重定向错误。 - dahe
你的 use App\Security\LoginFormAuthenticator; 的内容是什么? - allan.simon
在Symfony 4.4中与guards身份验证器一起使用的唯一解决方案是,避免在使用后者时出现“找不到类“Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken”的令牌的任何身份验证提供程序" - Laurent W.

4
如果有人有跟我一样的后续问题,需要重复查看这里:

调用

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

只会影响当前使用的路由的security.context

即,您只能从防火墙控制下的 URL 登录用户。

(如果需要,可以为路由添加一个异常 - IS_AUTHENTICATED_ANONYMOUSLY


1
你知道如何为会话(session)做到这一点吗?而不仅仅是针对当前请求(request)? - Jake N

2
正如Problematic在这里已经提到的那样,这个难以捉摸的$providerKey参数实际上不过是您防火墙规则的名称,在下面的示例中为'foobar'。
firewalls:
    foobar:
        pattern:    /foo/

你能解释一下为什么如果我将任何字符串例如 blablabla 作为 UsernamePasswordToken 的第三个参数传递,它也会起作用吗?这个参数是什么意思? - Mikhail
1
此参数将您的令牌绑定到特定的防火墙提供商。在大多数情况下,您只会有一个提供商,所以不必担心它。 - Gottlieb Notschnabel

2

我尝试了这里的所有答案,但都没有起作用。我唯一能在控制器上验证我的用户的方法是通过进行子请求,然后重定向。以下是我的代码,我正在使用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'));

1
在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")


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接