简体   繁体   English

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

[英]Symfony & Guard: “The security token was removed due to an AccountStatusException”

I tried to create an authenticator for my login form, but I always am unlogged for some unclear reason. 我尝试为我的登录表单创建一个身份验证器,但由于一些不明原因,我总是没有记录。

[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)"} []

I don't understand this "AuthenticationExpiredException" as I have nothing stateless, nor any expiration in any way nowhere in my app. 我不理解这个“AuthenticationExpiredException”,因为我没有任何无状态,​​也没有在我的应用程序中任何方式的任何过期。

Does this issue speak to anyone? 这个问题对任何人都有用吗?


Edit 1 编辑1

After a bunch of hours, it looks like I am unlogged because of the {{ is_granted('ROLE_USER') }} in Twig. 经过一段时间后,由于Twig中的{{ is_granted('ROLE_USER') }} ,我看起来好像没有记录。 Don't see why anyway. 反正不明白为什么。

Edit 2 编辑2

If I dump() my security token on the onAuthenticationSuccess authenticator's method, authenticated = true . 如果我在onAuthenticationSuccess身份验证器的方法上转储我的安全令牌,则authenticated = true

But, If I dump() my security token after a redirect or when accessing a new page, 'authenticated' = false . 但是,如果我在重定向或访问新页面后转储()我的安全令牌, 'authenticated' = false

Why the hell my authentication isn't stored. 为什么我的身份验证没有存储。


app/config/security.yml 应用程序/配置/ 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" }

AppBundle/Controller/SecurityController.php 的appbundle /控制器/ 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(),
        ];
    }
}

AppBundle/Form/Type/ConnectType.php 的appbundle /表/类型/ 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',
           ])
        ;
    }
}

AppBundle/Security/Authenticator/FormLoginAuthenticator.php 的appbundle /安全/认证者/被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')
        ;
    }
}

AppBundle/Security/Provider/MemberProvider.php 的appbundle /安全/供应商/ 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;
    }
}

AppBundle/Security/User/Member.php 的appbundle /安全/用户/ 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;
    }
}

src/AppBundle/Resources/config/services.yml SRC /的appbundle /资源/配置/ 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"]

I found my bug, after 8 hours of hard work. 经过8个小时的努力,我发现了我的虫子。 I promise, I'll drink a bulk of beers after this comment! 我保证,在评论之后,我会喝大量的啤酒!

I located my issue in the Symfony\\Component\\Security\\Core\\Authentication\\Token\\AbstractToken::hasUserChanged() method, which compares user stored in the session, and the one returned by the refreshUser of your provider. 我将我的问题放在Symfony\\Component\\Security\\Core\\Authentication\\Token\\AbstractToken::hasUserChanged()方法中,该方法比较存储在会话中的用户和提供者的refreshUser返回的refreshUser

My user entity was considered changed because of this condition: 由于这种情况,我的用户实体被视为已更改

    if ($this->user->getPassword() !== $user->getPassword()) {
        return true;
    }

In fact, before being stored in the session, the eraseCredentials() method is called on your user entity so the password is removed. 实际上,在存储在会话中之前,会在用户实体上调用eraseCredentials()方法,以便删除密码。 But the password exists in the user the provider returns. 但密码存在于提供程序返回的用户中。

That's why in documentations, they show plainPassword and password properties... They keep password in the session, and eraseCredentials just cleans up `plainPassword. 这就是为什么在文档中,它们显示了plainPasswordpassword属性......它们在会话中保留password ,eraseCredentials只清理`plainPassword。 Kind of tricky. 有点棘手。

Se we have 2 solutions: 我们有2个解决方案:

  • having eraseCredentials not touching password, can be useful if you want to unauthent your member when he changes his password somehow. 如果您想以某种方式更改密码时取消您的会员身份,则eraseCredentials不会触及密码会非常有用。

  • implementing EquatableInterface in our user entity, because the following test is called before the one above. 在我们的用户实体中实现EquatableInterface ,因为在上面的测试之前调用了以下测试。

     if ($this->user instanceof EquatableInterface) { return !(bool) $this->user->isEqualTo($user); } 

I decided to implement EquatableInterface in my user entity, and I'll never forget to do it in the future. 我决定在我的用户实体中实现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();
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM