Symfony&Guard:"由于AccountStatusException,安全令牌已被删除"

Ala*_*blo 8 php security symfony

我尝试为我的登录表单创建一个身份验证器,但由于一些不明原因,我总是没有记录.

[2016-10-05 18:54:53] security.INFO: Guard authentication successful! {"token":"[object] (Symfony\\Component\\Security\\Guard\\Token\\PostAuthenticationGuardToken: PostAuthenticationGuardToken(user=\"test@test.test\", authenticated=true, roles=\"ROLE_USER\"))","authenticator":"AppBundle\\Security\\Authenticator\\FormLoginAuthenticator"} []
[2016-10-05 18:54:54] security.INFO: An AuthenticationException was thrown; redirecting to authentication entry point. {"exception":"[object] (Symfony\\Component\\Security\\Core\\Exception\\AuthenticationExpiredException(code: 0):  at /space/products/insurance/vendor/symfony/symfony/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php:86)"} []
[2016-10-05 18:54:54] security.INFO: The security token was removed due to an AccountStatusException. {"exception":"[object] (Symfony\\Component\\Security\\Core\\Exception\\AuthenticationExpiredException(code: 0):  at /space/products/insurance/vendor/symfony/symfony/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php:86)"} []
Run Code Online (Sandbox Code Playgroud)

我不理解这个"AuthenticationExpiredException",因为我没有任何无状态,​​也没有在我的应用程序中任何方式的任何过期.

这个问题对任何人都有用吗?


编辑1

经过一段时间,看起来我因为{{ is_granted('ROLE_USER') }}在Twig中而未被记录.反正不明白为什么.

编辑2

如果我在onAuthenticationSuccess身份验证器的方法上转储我的安全令牌,authenticated = true.

但是,如果我在重定向或访问新页面后转储()我的安全令牌,'authenticated' = false.

为什么我的身份验证没有存储.


app/config/security.yml

security:

    encoders:
        AppBundle\Security\User\Member:
            algorithm: bcrypt
            cost: 12

    providers:
        members:
            id: app.provider.member

    role_hierarchy:
        ROLE_ADMIN:       "ROLE_USER"

    firewalls:
        dev:
            pattern: "^/(_(profiler|wdt|error)|css|images|js)/"
            security: false

        main:
            pattern: "^/"
            anonymous: ~
            logout: ~
            guard:
                authenticators:
                    - app.authenticator.form_login

    access_control:
        - { path: "^/connect", role: "IS_AUTHENTICATED_ANONYMOUSLY" }
        - { path: "^/register", role: "IS_AUTHENTICATED_ANONYMOUSLY" }
        - { path: "^/admin", role: "ROLE_ADMIN" }
        - { path: "^/user", role: "ROLE_USER" }
        - { path: "^/logout", role: "ROLE_USER" }
Run Code Online (Sandbox Code Playgroud)

AppBundle/Controller/SecurityController.php

<?php

namespace AppBundle\Controller;

use AppBundle\Base\BaseController;
use AppBundle\Form\Type\ConnectType;
use AppBundle\Security\User\Member;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;

class SecurityController extends BaseController
{
    /**
     * @Route("/connect", name="security_connect")
     * @Template()
     */
    public function connectAction(Request $request)
    {
        $connectForm = $this
           ->createForm(ConnectType::class)
           ->handleRequest($request)
        ;

        return [
            'connect' => $connectForm->createView(),
        ];
    }
}
Run Code Online (Sandbox Code Playgroud)

AppBundle/Form/Type/ConnectType.php

<?php

namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Validator\Constraints;
use EWZ\Bundle\RecaptchaBundle\Form\Type\EWZRecaptchaType;
use EWZ\Bundle\RecaptchaBundle\Validator\Constraints\IsTrue as RecaptchaTrue;

class ConnectType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
           ->add('email', Type\EmailType::class, [
               'label'    => 'Your email',
               'required' => true,
               'constraints' => [
                   new Constraints\Length(['min' => 8])
               ],
           ])
           ->add('password', Type\PasswordType::class, [
                'label'       => 'Your password',
                'constraints' => new Constraints\Length(['min' => 8, 'max' => 4096]), /* CVE-2013-5750 */
            ])
           ->add('recaptcha', EWZRecaptchaType::class, [
               'label'       => 'Please tick the checkbox below',
               'constraints' => [
                   new RecaptchaTrue()
               ],
           ])
           ->add('submit', Type\SubmitType::class, [
               'label' => 'Connect',
           ])
        ;
    }
}
Run Code Online (Sandbox Code Playgroud)

AppBundle/Security/Authenticator/FormLoginAuthenticator.php

<?php

namespace AppBundle\Security\Authenticator;

use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use AppBundle\Form\Type\ConnectType;

class FormLoginAuthenticator extends AbstractFormLoginAuthenticator
{
    private $container; // ¯\_(?)_/¯

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public function getCredentials(Request $request)
    {
        if ($request->getPathInfo() !== '/connect') {
            return null;
        }

        $connectForm = $this
           ->container
           ->get('form.factory')
           ->create(ConnectType::class)
           ->handleRequest($request)
        ;

        if ($connectForm->isValid()) {
            $data = $connectForm->getData();

            return [
                'username' => $data['email'],
                'password' => $data['password'],
            ];
        }

        return null;
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        return $userProvider->loadUserByUsername($credentials['username']);
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        $isValid = $this
           ->container
           ->get('security.password_encoder')
           ->isPasswordValid($user, $credentials['password'])
        ;

        if (!$isValid) {
            throw new BadCredentialsException();
        }

        return true;
    }

    protected function getLoginUrl()
    {
        return $this
           ->container
           ->get('router')
           ->generate('security_connect')
        ;
    }

    protected function getDefaultSuccessRedirectUrl()
    {
        return $this
           ->container
           ->get('router')
           ->generate('home')
        ;
    }
}
Run Code Online (Sandbox Code Playgroud)

AppBundle/Security/Provider/MemberProvider.php

<?php

namespace AppBundle\Security\Provider;

use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use AppBundle\Security\User\Member;
use Api\Gateway\RequestResponse\RequestResponseHandlerInterface;
use Api\Business\InsuranceWebsite\Action\GetInsuranceMember\GetInsuranceMemberRequest;
use Api\Gateway\Exception\NoResultException;

class MemberProvider implements UserProviderInterface
{
    protected $gateway;

    public function __construct(RequestResponseHandlerInterface $gateway)
    {
        $this->gateway = $gateway;
    }

    public function loadUserByUsername($username)
    {
        try {
            $response = $this->gateway->handle(
               new GetInsuranceMemberRequest($username)
            );
        } catch (NoResultException $ex) {
            throw new UsernameNotFoundException(
                sprintf('Username "%s" does not exist.', $username)
            );
        }

        $member = new Member();
        $member->setId($response->getId());
        $member->setUsername($response->getEmail());
        $member->setPassword($response->getPassword());
        $member->setCompanyId($response->getCompanyId());
        $member->setFirstname($response->getFirstname());
        $member->setLastname($response->getLastname());
        $member->setIsManager($response->isManager());
        $member->setIsEnabled($response->isEnabled());

        return $member;
    }

    public function refreshUser(UserInterface $user)
    {
        if (!$user instanceof Member) {
            throw new UnsupportedUserException(
                sprintf('Instances of "%s" are not supported.', get_class($user))
            );
        }

        return $this->loadUserByUsername($user->getUsername());
    }

    public function supportsClass($class)
    {
        return $class === Member::class;
    }
}
Run Code Online (Sandbox Code Playgroud)

AppBundle/Security/User/Member.php

<?php

namespace AppBundle\Security\User;

use Symfony\Component\Security\Core\User\UserInterface;

class Member implements UserInterface
{
    private $id;
    private $username;
    private $password;
    private $companyId;
    private $firstname;
    private $lastname;
    private $isManager;
    private $isEnabled;
    private $roles = ['ROLE_USER'];

    public function getId()
    {
        return $this->id;
    }

    public function setId($id)
    {
        $this->id = $id;

        return $this;
    }

    public function getUsername()
    {
        return $this->username;
    }

    public function setUsername($username)
    {
        $this->username = $username;

        return $this;
    }

    public function getPassword()
    {
        return $this->password;
    }

    public function setPassword($password)
    {
        $this->password = $password;
        return $this;
    }

    public function getCompanyId()
    {
        return $this->companyId;
    }

    public function setCompanyId($companyId)
    {
        $this->companyId = $companyId;

        return $this;
    }

    public function getFirstname()
    {
        return $this->firstname;
    }

    public function setFirstname($firstname)
    {
        $this->firstname = $firstname;

        return $this;
    }

    public function getLastname()
    {
        return $this->lastname;
    }

    public function setLastname($lastname)
    {
        $this->lastname = $lastname;

        return $this;
    }

    public function isManager()
    {
        return $this->isManager;
    }

    public function setIsManager($isManager)
    {
        $this->isManager = $isManager;

        return $this;
    }

    public function IsEnabled()
    {
        return $this->isEnabled;
    }

    public function setIsEnabled($isEnabled)
    {
        $this->isEnabled = $isEnabled;

        return $this;
    }

    public function eraseCredentials()
    {
        $this->password = null;
    }

    public function hasRole($role)
    {
        return in_array($role, $this->roles);
    }

    public function getRoles()
    {
        return $this->roles;
    }

    public function addRole($role)
    {
        if (!$this->hasRole($role)) {
            $this->roles[] = $role;
        }

        return $this;
    }

    public function removeRole($role)
    {
        $index = array_search($role, $this->roles);
        if ($index !== false) {
            unset($this->roles[$index]);
            $this->roles = array_values($this->roles);
        }

        return $this;
    }

    public function getSalt()
    {
        return null;
    }
}
Run Code Online (Sandbox Code Playgroud)

src/AppBundle/Resources/config/services.yml

imports:

parameters:
    app.provider.member.class: AppBundle\Security\Provider\MemberProvider
    app.authenticator.form_login.class: AppBundle\Security\Authenticator\FormLoginAuthenticator

services:
    app.provider.member:
        class: %app.provider.member.class%
        arguments: ['@gateway']

    app.authenticator.form_login:
        class: %app.authenticator.form_login.class%
        arguments: ["@service_container"]
Run Code Online (Sandbox Code Playgroud)

Ala*_*blo 23

经过8个小时的努力,我发现了我的虫子.我保证,在评论之后,我会喝大量的啤酒!

我在Symfony\Component\Security\Core\Authentication\Token\AbstractToken::hasUserChanged()方法中找到了我的问题,该方法比较了会话中存储的用户和refreshUser提供者返回的用户.

由于这种情况,我的用户实体被视为已更改:

    if ($this->user->getPassword() !== $user->getPassword()) {
        return true;
    }
Run Code Online (Sandbox Code Playgroud)

实际上,在存储在会话中之前,会eraseCredentials()在您的用户实体上调用该方法,以便删除密码.但密码存在于提供程序返回的用户中.

这就是为什么在文档中,它们显示plainPasswordpassword属性......它们保持password在会话中,eraseCredentials只是清理`plainPassword.有点棘手.

我们有2个解决方案:

  • eraseCredentials没有碰到密码,如果你想unauthent您的会员时,他莫名其妙地改变了他的密码,可能是有用的.

  • 实现EquatableInterface我们的用户实体,因为下面的测试上面的那个叫之前.

    if ($this->user instanceof EquatableInterface) {
        return !(bool) $this->user->isEqualTo($user);
    }
    
    Run Code Online (Sandbox Code Playgroud)

我决定EquatableInterface在我的用户实体中实现,我将永远不会忘记这样做.

<?php

namespace AppBundle\Security\User;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\EquatableInterface;

class Member implements UserInterface, EquatableInterface
{

    // (...)

    public function isEqualTo(UserInterface $user)
    {
        return $user->getId() === $this->getId();
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 嘿,我知道这篇文章很旧,但我只想说“谢谢”。我最近不得不更新旧的 symfony2 系统,并且遇到了与所描述的相同的问题。你的帖子和更新为我节省了很多工作:)非常感谢! (2认同)