令牌在尝试刷新后被取消身份验证

Posted

技术标签:

【中文标题】令牌在尝试刷新后被取消身份验证【英文标题】:Token was deauthenticated after trying to refresh it 【发布时间】:2018-05-20 09:27:23 【问题描述】:

我将 Symfony 环境从 3.3 更新到 4.0。更新后我的登录有问题(数据库提供的用户)。当我提交登录表单时,我刚刚回到登录表单,没有任何错误消息。当我使用无效凭据时,我收到了相应的错误消息。这是尝试登录后的日志。使用“in_memory”用户提供程序的登录正在工作。你需要更多的信息?

[2017-12-06 13:57:05] security.INFO: User has been authenticated successfully. "username":"***" []
[2017-12-06 14:22:39] doctrine.DEBUG: "START TRANSACTION" [] []
[2017-12-06 13:57:05] security.DEBUG: Read existing security token from the session. "key":"_security_secured_area","token_class":"Symfony\\Component\\Security\\Core\\Authentication\\Token\\UsernamePasswordToken" []
[2017-12-06 13:57:05] doctrine.DEBUG: SELECT t0.username AS username_1, t0.password AS password_2, t0.email AS email_3, t0.email_new AS email_new_4, t0.first_name AS first_name_5, t0.last_name AS last_name_6, t0.is_active AS is_active_7, t0.email_confirmed AS email_confirmed_8, t0.shibboleth_state AS shibboleth_state_9, t0.shibboleth_hash AS shibboleth_hash_10, t0.shibboleth_persistent_id AS shibboleth_persistent_id_11, t0.confirmation_email_send AS confirmation_email_send_12, t0.last_login AS last_login_13, t0.expires AS expires_14, t0.session_id AS session_id_15, t0.id AS id_16, t0.hidden AS hidden_17, t0.deleted AS deleted_18, t0.created AS created_19, t0.modified AS modified_20, t0.sorting AS sorting_21, t0.salutation_id AS salutation_id_22, t0.creator_id AS creator_id_23, t0.modifier_id AS modifier_id_24 FROM User t0 WHERE t0.id = ? AND ((t0.deleted = 0)) [2] []
[2017-12-06 13:57:05] security.DEBUG: Token was deauthenticated after trying to refresh it. "username":"user","provider":"Symfony\\Component\\Security\\Core\\User\\ChainUserProvider" []
[2017-12-06 13:57:05] security.INFO: Populated the TokenStorage with an anonymous Token. [] []
[2017-12-06 13:57:05] security.DEBUG: Access denied, the user is not fully authenticated; redirecting to authentication entry point. "exception":"[object] (Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException(code: 403): Access Denied. at /vendor/symfony/symfony/src/Symfony/Component/Security/Http/Firewall/AccessListener.php:68)" []
[2017-12-06 13:57:05] security.DEBUG: Calling Authentication entry point. [] []

实体\用户:

class User extends EntitySuperclass implements AdvancedUserInterface, \Serializable

    /**
     * @ORM\Column(type="string")
     */
    private $username;

    /**
     *
     * @Assert\Length(max=4096,groups="account_complete","account_password","user")
     * @Assert\Length(min = 8,groups="account_complete","account_password","user", minMessage="user.password_length")
     */
    private $plainPassword;

    /**
     * The below length depends on the "algorithm" you use for encoding
     * the password, but this works well with bcrypt.
     *
     * @ORM\Column(type="string", length=64)
     */
    private $password;

    /**
     * @ORM\Column(type="string", length=255)
     * @Assert\NotBlank(groups="account_register","user")
     * @Assert\Email(
     *      groups = "account_register", "account","user",
     *      strict = true,
     *      checkMX = true
     * )
     */
    private $email;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $emailNew = '';

    /**
     * @ORM\ManyToOne(targetEntity="Salutation")
     * 
     */
    private $salutation;

    /**
     * @ORM\Column(type="string")
     * @Assert\NotBlank(groups="account_complete","user")
     * @Assert\Regex(pattern = "/^[a-zA-ZäöüÄÖÜß0-9 ]+$/",groups="account_complete","user", message="user.first_name.regex")
     */
    private $firstName;

    /**
     * @ORM\Column(type="string")
     * @Assert\NotBlank(groups="account_complete","user")
     * @Assert\Regex(pattern = "/^[a-zA-ZäöüÄÖÜß0-9 ]+$/",groups="account_complete","user", message="user.last_name.regex")
     */
    private $lastName;

    /**
     * @ORM\Column(name="is_active", type="boolean")
     */
    private $isActive = false;

    /**
     * @ORM\Column(name="email_confirmed", type="boolean")
     */
    private $emailConfirmed = false;

    /**
     * @ORM\Column(type="integer")
     */
    private $shibbolethState = 0;

    /**
     * @ORM\Column(type="string")
     */
    private $shibbolethHash = '';

    /**
     * @ORM\Column(type="string")
     */
    private $shibbolethPersistentId = '';

    /**
     * @ORM\ManyToMany(targetEntity="UserGroup")
     * @ORM\JoinTable(name="User_UserGroup",
     *      joinColumns=@ORM\JoinColumn(name="user_id", referencedColumnName="id"),
     *      inverseJoinColumns=@ORM\JoinColumn(name="group_id", referencedColumnName="id")
     *      )
     */
    private $userGroups;

    /**
     * @ORM\Column(type="integer")
     */
    private $confirmationEmailSend;

    /**
     * @ORM\Column(type="integer")
     */
    private $lastLogin = 0;

    /**
     * @ORM\Column(type="integer")
     */
    protected $expires = 0;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $sessionId = '';

    /**
     * @ORM\ManyToMany(targetEntity="BankDetails", cascade="persist")
     * @ORM\JoinTable(name="User_BankDetails",
     *      joinColumns=@ORM\JoinColumn(name="user_id", referencedColumnName="id"),
     *      inverseJoinColumns=@ORM\JoinColumn(name="bank_details_id", referencedColumnName="id")
     * )
     * @Assert\Valid
     */
    private $bankDetails;

    /**
     * @ORM\ManyToMany(targetEntity="Address", cascade="persist")
     * @ORM\JoinTable(name="User_BillingAddress",
     *      joinColumns=@ORM\JoinColumn(name="user_id", referencedColumnName="id"),
     *      inverseJoinColumns=@ORM\JoinColumn(name="billing_address_id", referencedColumnName="id")
     * )
     * @Assert\Count(
     *      min = 1,
     *      minMessage = "user.billing_addresses.min",
     * )
     * @Assert\Valid
     */
    private $billingAddresses;

    public function __construct()
    
        parent::__construct();
        $this->isActive = true;
        $this->confirmationEmailSend = 0;
        $this->userGroups = new ArrayCollection();
        $this->bankDetails = new ArrayCollection();
        $this->billingAddresses = new ArrayCollection();
        // may not be needed, see section on salt below
        // $this->salt = md5(uniqid(null, true));
    

    /**
     * @ORM\PrePersist
     */
    public function prePersist()
    
        $currentTimestamp = time();

        if($this->getConfirmationEmailSend() == NULL)
            $this->setConfirmationEmailSend(0);

   

    public function getUsername()
    
        //return $this->username;
        return $this->email;
    

    public function getSalt()
    
        // The bcrypt algorithm doesn't require a separate salt.
        return null;
    

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

    public function getRoles()
    
        $roles = array();
        $userGroups = $this->getUserGroups();
        if(!empty($userGroups)) 
            foreach($userGroups as $userGroup) 
                $role = $userGroup->getRole();
                $roles[] = 'ROLE_'.strtoupper($role);
            
        
        return $roles;
    

    public function isGranted($role)
    
        return in_array($role, $this->getRoles());
    

    public function eraseCredentials()
    
    

    public function isAccountNonExpired()
    
        return true;
    

    public function isAccountNonLocked()
    
        return true;
    

    public function isCredentialsNonExpired()
    
        return true;
    

    public function isEnabled()
    
        return $this->isActive;
    

    /** @see \Serializable::serialize() */
    public function serialize()
    
        return serialize(array(
            $this->id,
            $this->username,
            $this->password,
            $this->isActive,
            // see section on salt below
            // $this->salt,
        ));
    

    /** @see \Serializable::unserialize() */
    public function unserialize($serialized)
    
        list (
            $this->id,
            $this->username,
            $this->password,
            $this->isActive,
            // see section on salt below
            // $this->salt
        ) = unserialize($serialized);
    

    /**
     * Set username
     *
     * @param string $username
     *
     * @return User
     */
    public function setUsername($username)
    
        $this->username = $username;

        return $this;
    

    public function getPlainPassword()
    
        return $this->plainPassword;
    

    public function setPlainPassword($password)
    
        $this->plainPassword = $password;
    

    /**
     * Set password
     *
     * @param string $password
     *
     * @return User
     */
    public function setPassword($password)
    
        $this->password = $password;

        return $this;
    

    /**
     * Set email
     *
     * @param string $email
     *
     * @return User
     */
    public function setEmail($email)
    
        $this->email = $email;
        $this->setUsername($email);

        return $this;
    

    /**
     * Get email
     *
     * @return string
     */
    public function getEmail()
    
        return $this->email;
    

    /**
     * Set isActive
     *
     * @param boolean $isActive
     *
     * @return User
     */
    public function setIsActive($isActive)
    
        $this->isActive = $isActive;

        return $this;
    

    /**
     * Get isActive
     *
     * @return boolean
     */
    public function getIsActive()
    
        return $this->isActive;
    

    /**
     * Add userGroup
     *
     * @param \AppBundle\Entity\UserGroup $userGroup
     *
     * @return User
     */
    public function addUserGroup(\AppBundle\Entity\UserGroup $userGroup)
    
        $this->userGroups[] = $userGroup;

        return $this;
    

    /**
     * Remove userGroup
     *
     * @param \AppBundle\Entity\UserGroup $userGroup
     */
    public function removeUserGroup(\AppBundle\Entity\UserGroup $userGroup)
    
        $this->userGroups->removeElement($userGroup);
    

    /**
     * Get userGroups
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getUserGroups()
    
        return $this->userGroups;
    

    /**
     * Set shibbolethPersistentId
     *
     * @param string $shibbolethPersistentId
     *
     * @return User
     */
    public function setShibbolethPersistentId($shibbolethPersistentId)
    
        $this->shibbolethPersistentId = $shibbolethPersistentId;

        return $this;
    

    /**
     * Get shibbolethPersistentId
     *
     * @return string
     */
    public function getShibbolethPersistentId()
    
        return $this->shibbolethPersistentId;
    

    /**
     * Set firstName
     *
     * @param string $firstName
     *
     * @return User
     */
    public function setFirstName($firstName)
    
        $this->firstName = $firstName;

        return $this;
    

    /**
     * Get firstName
     *
     * @return string
     */
    public function getFirstName()
    
        return $this->firstName;
    

    /**
     * Set lastName
     *
     * @param string $lastName
     *
     * @return User
     */
    public function setLastName($lastName)
    
        $this->lastName = $lastName;

        return $this;
    

    /**
     * Get lastName
     *
     * @return string
     */
    public function getLastName()
    
        return $this->lastName;
    

    /**
     * Set emailConfirmed
     *
     * @param boolean $emailConfirmed
     *
     * @return User
     */
    public function setEmailConfirmed($emailConfirmed)
    
        $this->emailConfirmed = $emailConfirmed;

        return $this;
    

    /**
     * Get emailConfirmed
     *
     * @return boolean
     */
    public function getEmailConfirmed()
    
        return $this->emailConfirmed;
    

    public function removeAllUserGroups() 
        $userGroups = $this->getUserGroups();
        foreach($userGroups as $userGroup) 
           $this->removeUserGroup($userGroup);
        
    

    public function hasUserGroup($userGroupId) 
        foreach($this->getUserGroups() as $userGroup) 
            if($userGroup->getId() == $userGroupId)
                return true;
        
        return false;
    

    /**
     * Set lastLogin
     *
     * @param integer $lastLogin
     *
     * @return User
     */
    public function setLastLogin($lastLogin)
    
        $this->lastLogin = $lastLogin;

        return $this;
    

    /**
     * Get lastLogin
     *
     * @return integer
     */
    public function getLastLogin()
    
        return $this->lastLogin;
    

    /**
     * Set confirmationEmailSend
     *
     * @param integer $confirmationEmailSend
     *
     * @return User
     */
    public function setConfirmationEmailSend($confirmationEmailSend)
    
        $this->confirmationEmailSend = $confirmationEmailSend;

        return $this;
    

    /**
     * Get confirmationEmailSend
     *
     * @return integer
     */
    public function getConfirmationEmailSend()
    
        return $this->confirmationEmailSend;
    

    /**
     * Set validTill
     *
     * @param integer $validTill
     *
     * @return User
     */
    public function setValidTill($validTill)
    
        $this->validTill = $validTill;

        return $this;
    

    /**
     * Get validTill
     *
     * @return integer
     */
    public function getValidTill()
    
        return $this->validTill;
    

    /**
     * Set shibbolethValid
     *
     * @param integer $shibbolethValid
     *
     * @return User
     */
    public function setShibbolethValid($shibbolethValid)
    
        $this->shibbolethValid = $shibbolethValid;

        return $this;
    

    /**
     * Get shibbolethValid
     *
     * @return integer
     */
    public function getShibbolethValid()
    
        return $this->shibbolethValid;
    

    /**
     * Set shibbolethHash
     *
     * @param string $shibbolethHash
     *
     * @return User
     */
    public function setShibbolethHash($shibbolethHash)
    
        $this->shibbolethHash = $shibbolethHash;

        return $this;
    

    /**
     * Get shibbolethHash
     *
     * @return string
     */
    public function getShibbolethHash()
    
        return $this->shibbolethHash;
    

    /**
     * Set shibbolethState
     *
     * @param integer $shibbolethState
     *
     * @return User
     */
    public function setShibbolethState($shibbolethState)
    
        $this->shibbolethState = $shibbolethState;

        return $this;
    

    /**
     * Get shibbolethState
     *
     * @return integer
     */
    public function getShibbolethState()
    
        return $this->shibbolethState;
    

    /**
     * Set expires
     *
     * @param integer $expires
     *
     * @return User
     */
    public function setExpires($expires)
    
        $this->expires = $expires;

        return $this;
    

    /**
     * Get expires
     *
     * @return integer
     */
    public function getExpires()
    
        return $this->expires;
    

    /**
     * Set emailNew
     *
     * @param string $emailNew
     *
     * @return User
     */
    public function setEmailNew($emailNew)
    
        $this->emailNew = $emailNew;

        return $this;
    

    /**
     * Get emailNew
     *
     * @return string
     */
    public function getEmailNew()
    
        return $this->emailNew;
    

    /**
     * Set passwordHash
     *
     * @param string $passwordHash
     *
     * @return User
     */
    public function setPasswordHash($passwordHash)
    
        $this->passwordHash = $passwordHash;

        return $this;
    

    /**
     * Get passwordHash
     *
     * @return string
     */
    public function getPasswordHash()
    
        return $this->passwordHash;
    

    /**
     * Set sessionId
     *
     * @param string $sessionId
     *
     * @return User
     */
    public function setSessionId($sessionId)
    
        $this->sessionId = $sessionId;

        return $this;
    

    /**
     * Get sessionId
     *
     * @return string
     */
    public function getSessionId()
    
        return $this->sessionId;
    

    /**
     * Set salutation
     *
     * @param \AppBundle\Entity\Salutation $salutation
     *
     * @return User
     */
    public function setSalutation(\AppBundle\Entity\Salutation $salutation = null)
    
        $this->salutation = $salutation;

        return $this;
    

    /**
     * Get salutation
     *
     * @return \AppBundle\Entity\Salutation
     */
    public function getSalutation()
    
        return $this->salutation;
    

    /**
     * Add bankDetail
     *
     * @param \AppBundle\Entity\BankDetails $bankDetail
     *
     * @return User
     */
    public function addBankDetail(\AppBundle\Entity\BankDetails $bankDetail)
    
        $this->bankDetails[] = $bankDetail;

        return $this;
    

    /**
     * Remove bankDetail
     *
     * @param \AppBundle\Entity\BankDetails $bankDetail
     */
    public function removeBankDetail(\AppBundle\Entity\BankDetails $bankDetail)
    
        $this->bankDetails->removeElement($bankDetail);
    

    /**
     * Get bankDetails
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getBankDetails()
    
        return $this->bankDetails;
    

    /**
     * Add billingAddress
     *
     * @param \AppBundle\Entity\Address $billingAddress
     *
     * @return User
     */
    public function addBillingAddress(\AppBundle\Entity\Address $billingAddress)
    
        $this->billingAddresses[] = $billingAddress;

        return $this;
    

    /**
     * Remove billingAddress
     *
     * @param \AppBundle\Entity\Address $billingAddress
     */
    public function removeBillingAddress(\AppBundle\Entity\Address $billingAddress)
    
        $this->billingAddresses->removeElement($billingAddress);
    

    /**
     * Set billingAddresses
     *
     * @param \AppBundle\Entity\Address $billingAddress
     *
     * @return User
     * 
     */
    public function setBillingAddresses(\AppBundle\Entity\Address $billingAddress)
    
        if($this->billingAddresses !== NULL and $this->billingAddresses->contains($billingAddress))
            return false;
        
        $this->addBillingAddress($billingAddress);
        return $this;
    

    /**
     * Set one billingAddresses
     *
     * @param \AppBundle\Entity\Address $billingAddress
     *
     * @return User
     * 
     */
    public function setOneBillingAddresses(\AppBundle\Entity\Address $billingAddress)
    
        $this->billingAddresses = $billingAddress;

        return $this;
    

    /**
     * Set one billingAddresses
     *
     * @param \AppBundle\Entity\Address $billingAddress
     *
     * @return User
     * 
     */
    public function unsetBillingAddresses()
    
        $this->billingAddresses = new ArrayCollection();

        return $this;
    

    /**
     * Get billingAddresses
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getBillingAddresses()
    
        return $this->billingAddresses;
    

config/security.yml

providers:
        chain_provider:
            chain:
                providers: [in_memory, database_user]
        in_memory:
            memory:
                users:
                    admin:
                        password: ***
                        roles: 'ROLE_ADMIN'
        database_user:
            entity:
                class: AppBundle:User

    firewalls:
        # disables authentication for assets and the profiler, adapt it according to your needs
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        secured_area:
            # pattern: match to pages
            anonymous: ~
            pattern:    ^/
            access_denied_handler: AppBundle\Security\AccessDeniedHandler
            provider: chain_provider
            form_login:
                login_path: /login
                check_path: /login_check
                default_target_path: account
                # Configuring CSRF protection
                csrf_parameter: _csrf_security_token
                csrf_token_id: a_private_string
                success_handler: AppBundle\Handler\LoginSuccessHandler
            logout:
                path: /logout
                target: /login

    access_control:
        ...

    role_hierarchy:
        ...

    encoders:
        AppBundle\Entity\User:
            algorithm: bcrypt
        Symfony\Component\Security\Core\User\User:
            plaintext

【问题讨论】:

是的,我确认 Serializable 接口可以工作,但你应该比较 $id、$username 和 $password 是否可以工作,但是对于 Symfony 4,你应该在内部实现 EquatableInterface 和 isEqualTo 方法。 Symfony 没有更新,我创建了 pr github.com/symfony/symfony-docs/pull/9914 【参考方案1】:

从 Symfony 4.0 开始,logout_on_user_change 设置为 true。这意味着如果用户被更改,用户将被注销。

您应该实现Symfony\Component\Security\Core\User\EquatableInterface 并添加isEqualTo 方法:

class User implements EquatableInterface

    public function isEqualTo(UserInterface $user)
    
        if ($this->password !== $user->getPassword()) 
            return false;
        

        if ($this->salt !== $user->getSalt()) 
            return false;
        

        if ($this->username !== $user->getUsername()) 
            return false;
        

        return true;
    

更新日志

https://github.com/symfony/security-bundle/blob/master/CHANGELOG.md

4.1.0

logout_on_user_change 防火墙选项已弃用,将在 5.0 中删除。

4.0.0

防火墙选项logout_on_user_change 现在始终为真,如果用户在请求之间发生变化,这将触发注销

3.4.0

在防火墙选项中添加了logout_on_user_change。当用户更改时,此配置项将触发注销。应设置为 true 以避免配置中的弃用。

在撰写此答案时尚未记录该选项:https://github.com/symfony/symfony-docs/issues/8428,但现在是:https://symfony.com/doc/4.4/reference/configuration/security.html#logout-on-user-change

关于更新到新的主要版本的附注

如果您想升级到新的主要版本,请始终先更新到最新的次要版本。这意味着在更新到 3.0 之前更新到 2.8 并在转到 4.0 之前更新到 3.4。请参阅 Fabien Potencier 的 Symfony 4: Compose your Applications。

Symfony 3.0 = Symfony 2.8 - 已弃用的功能

(..)

Symfony 4.0 = Symfony 3.4 - 弃用的功能 + 一种新的开发方式 应用

如果您已经在使用最新的次要版本,则更新到新的主要版本会容易得多,因为您可以看到所有弃用通知。

【讨论】:

非常感谢!我检查了代码几个小时,但自己没有找到任何东西!这不应该添加到entity provider的文档中吗? 不客气!几天前,我也搜索了几个小时,很高兴分享解决方案。我不知道应该记录在哪里,但至少应该在某个地方。 你好,我也有同样的问题,但这个解决方案不起作用:(有什么想法吗? 好吧,我终于找到了!在序列化函数中我不得不放盐,因为我使用 sha512 加密 @StephanVierkant 我遇到了与 ArGh 相同的问题,isEqualTo() 的字段必须与 serialize() 函数中的字段匹配【参考方案2】:

我遇到了 getRoles 函数的问题。我的用户没有任何角色

在 UsernamePasswordToken 中构造令牌时,如果有空角色,则令牌不会被验证:

class UsernamePasswordToken extends AbstractToken

..
    public function __construct($user, $credentials, string $providerKey, array $roles = [])
    
        parent::__construct($roles);
    ...

        parent::setAuthenticated(\count($roles) > 0);
    

换句话说,当用户的角色为空时,他没有经过身份验证。

我通过在我的用户类中编写 getRoles 来解决我的问题,就像当前文档 https://symfony.com/doc/current/security.html#roles 一样,以保证每个用户至少有 ROLE_USER

public function getRoles()

    $roles = $this->roles;
    $roles[] = 'ROLE_USER';    
    return array_unique($roles);

希望有所帮助。

【讨论】:

以上是关于令牌在尝试刷新后被取消身份验证的主要内容,如果未能解决你的问题,请参考以下文章

如果身份验证令牌在帐户身份验证器中过期,则使用刷新令牌

ASP.NET Core 中基于令牌的身份验证(刷新)

[在第二次尝试刷新令牌时如何修复“格式错误的身份验证代码”?

如何在 graphql 中为基于 jwt 的身份验证实现自动刷新令牌?

如何在okhttp3身份验证器中使用具有异步请求的刷新令牌添加身份验证

使用 Moya 刷新身份验证令牌