用户角色何时刷新,如何强制刷新?

21

首先,我没有使用FOSUserBundle,也不能使用它,因为我正在移植一个具有自己模型层(这里没有Doctrine/Mongo/任何其他的东西)和其他非常定制化行为的遗留系统。

我正在尝试将我的遗留角色系统与Symfony连接起来,以便我可以在控制器和视图中使用Symfony的原生安全性。

我的第一次尝试是在Symfony\Component\Security\Core\User\UserInterfacegetRoles()方法中加载并返回所有用户角色。起初看起来好像可以工作。但经过深入研究,我发现这些角色仅在用户登录时刷新。这意味着如果我赋予或撤销了用户的角色,则必须注销并重新登录才能使更改生效。然而,如果我从用户那里撤销安全角色,我希望立即应用该行为,因此这种行为对我来说是不可接受的。

我希望Symfony能够重新加载用户的角色以确保它们是最新的。我已经实现了一个自定义用户提供者,并且在每个请求上都会调用其refreshUser(UserInterface $user)方法,但角色似乎没有被刷新。

在我的UserProvider中加载/刷新用户的代码看起来像这样:

public function loadUserByUsername($username) {
    $user = UserModel::loadByUsername($username); // Loads a fresh user object including roles!
    if (!$user) {
        throw new UsernameNotFoundException("User not found");
    }
    return $user;
}

(refreshUser 看起来很相似)

有没有办法让Symfony在每个请求中刷新用户角色?

8个回答

24

在尝试寻找可行解决方案并向Symfony2用户邮件列表做出贡献的几天后,我终于找到了。以下内容是从讨论中获得的:https://groups.google.com/d/topic/symfony2/NDBb4JN3mNc/discussion

原来有一个接口Symfony\Component\Security\Core\User\EquatableInterface,它旨在用于比较对象是否在安全和重新验证上下文中相等,而不是比较对象标识。

在您的用户类(已经实现了UserInterface)中实现该接口。实现唯一必需的方法isEqualTo(UserInterface $user),以便在当前用户的角色与传递用户的角色不同时返回false。

注意:用户对象会被序列化到会话中。由于序列化的方式,确保将角色存储在用户对象的字段中,不要直接在getRoles()方法中检索它们,否则所有这些都将无法工作!

以下是特定方法的示例:

protected $roles = null;

public function getRoles() {

    if ($this->roles == null) {
        $this->roles = ...; // Retrieve the fresh list of roles
                            // from wherever they are stored here
    }

    return $this->roles;
}

public function isEqualTo(UserInterface $user) {

    if ($user instanceof YourUserClass) {
        // Check that the roles are the same, in any order
        $isEqual = count($this->getRoles()) == count($user->getRoles());
        if ($isEqual) {
            foreach($this->getRoles() as $role) {
                $isEqual = $isEqual && in_array($role, $user->getRoles());
            }
        }
        return $isEqual;
    }

    return false;
}

另外,请注意当角色实际更改并重新加载页面时,性能分析工具栏可能会告诉您您的用户未经过身份验证。此外,查看分析器,您可能会发现角色实际上没有刷新。

我发现角色刷新确实有效。只是如果没有命中任何授权限制(没有@Secure注释,在防火墙中没有所需的角色等),则实际上不会执行刷新操作,用户将保持“未经身份验证”的状态。

一旦您访问需要进行任何类型的授权检查的页面,用户角色将被刷新,并且性能分析工具栏会再次显示带有绿点和“已认证:是”的用户。

对我来说,这是可以接受的行为-希望这有所帮助:)


不要忘记实现 \Serializable 接口并包括 id、salt 和 isActive。由于您还需要检查角色是否更改,因此也将它添加到可序列化数据中。 - ken
1
太棒了!我已经实现了FOSUserBundle,它为我提供了FOS\UserBundle\Model\User,我的用户实体继承了它。我只是将EquatableInterface实现到其中,它就像魔法一样运行了(因为FOS的用户已经完成了序列化)。 - juuga
我不理解比较角色是否相等的意义,我只想刷新Symfony2神奇的角色刷新。 - delmalki
1
当您实现EquatableInterface时,Symfony将跳过所有其他检查以检查用户是否已更改(请参见https://github.com/symfony/security-core/blob/master/Authentication/Token/AbstractToken.php#L250)。所以我猜您必须在`isEqualTo`函数中自己完成所有这些事情? - vincecore
2
返回的文本如下: return count($this->getRoles()) == count($user->getRoles()) && count(array_diff($this->getRoles(), $user->getRoles())) == 0; - fracz
显示剩余4条评论

12

在您的security.yml文件中(或替代文件):

security:
    always_authenticate_before_granting: true

我这辈子玩的最简单的游戏。


不要忘记重建 bootstrap.php.cache。symfony3.2 - gam6itko

8

在控制器中,向用户添加角色并保存到数据库后,只需要调用以下命令:

// Force refresh of user roles
$token = $this->get('security.context')->getToken()->setAuthenticated(false);

4
对于我来说很有效,但如果您想避免在Symfony新版本中收到弃用通知,请使用security.token_storage代替security.context :) - inanimatt
在服务中,您可以添加事件“kernel.controller”来强制其他已登录的用户刷新权限角色。 - gastonnina

4

请看这里,在security.yml中将always_authenticate_before_granting设置为true


3

我通过实现自己的EntityUserProvider并重写loadByUsername($username)方法来实现这种行为:

   /**
    * Load an user from its username
    * @param string $username
    * @return UserInterface
    */
   public function loadUserByUsername($username)
   {
      $user = $this->repository->findOneByEmailJoinedToCustomerAccount($username);

      if (null === $user)
      {
         throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username));
      }

      //Custom function to definassigned roles to an user
      $roles = $this->loadRolesForUser($user);

      //Set roles to the user entity
      $user->setRoles($roles);

      return $user;
   }

关键是每次调用loadByUsername时都要调用setRoles...希望这能帮到你。

这个解决方案似乎与Doctrine绑定在一起。但我并没有使用Doctrine。而且,即使不考虑Doctrine,我也不明白在用户实体上设置属性如何影响Symfony安全上下文中的内容? - netmikey
用户对象被设置在令牌中,并在用于令牌的持久层中检索(会话/cookie/bdd)。如果您不每次刷新用户时特别刷新角色,则安全上下文将使用令牌角色(与PHP会话一起持久化)。让我们看看DaoAuthenticationProvider :: retirieveUser方法。无论您使用何种方式管理用户,都必须调整UserProvider :: loadByUsername方法以在每个请求上刷新角色。 - AlterPHP
是的,我明白你的意思:用户对象存储在令牌中,而令牌(在我的情况下)存储在用户会话中。但是这难道不正是 refreshUser(UserInterface $user) 方法的作用吗?我的 UserProvider 从数据库加载并返回一个新的用户(包括角色!),但它们在令牌中仍然没有得到刷新。 - netmikey
除非 roles 是您的用户对象的数组字段(DB列),否则它不会在您的 loadUserByUsername 方法中刷新... - AlterPHP
让我们在聊天中继续这个讨论。 - netmikey
显示剩余2条评论

1
解决方案是在Doctrine的postUpdate事件上挂上一个订阅者。如果更新的实体是用户,且与已登录的用户相同,则使用AuthenticationManager服务进行身份验证。当然,您需要将服务容器(或相关服务)注入到订阅者中。为了避免循环引用问题,我更喜欢将整个容器注入。
public function postUpdate(LifecycleEventArgs $ev) {
    $entity = $ev->getEntity();

    if ($entity instanceof User) {
        $sc = $this->container->get('security.context');
        $user = $sc->getToken()->getUser();

        if ($user === $entity) {
            $token = $this->container->get('security.authentication.manager')->authenticate($sc->getToken());

            if ($token instanceof TokenInterface) {
                $sc->setToken($token);
            }
        }
    }
}

0

抱歉,我无法在评论中回复,所以我回答问题。如果有人在Symfony安全性方面是新手,并尝试在自定义密码身份验证中使角色刷新工作,则需要在authenticateToken函数内部进行操作:

if(count($token->getRoles()) > 0 ){
        if ($token->getUser() == $user ){
            $passwordValid=true;
        }
    }

不要从数据库/LDAP或其他任何地方检查密码。如果用户进入系统,则$token中仅包含用户名,没有角色。


0

我一直在为Symfony4这个问题而奋斗,现在终于找到了解决方案。

问题是,在我的情况下,角色取决于用户所属的“公司”。他可能是一家公司的首席执行官,但在另一家公司中是操作员,而菜单、权限等都取决于公司。当切换公司时,用户不应该重新登录。

最后我做了以下几点:

  • 将防火墙设置为无状态。
  • 在FormAuthentication类中,我明确地在会话中设置了一个属性,即用户名。
  • 我设置了另一个Guard,它基本上使用这个属性,并在每个请求中从数据库加载相应的用户。
class FormAuthenticator extends AbstractFormLoginAuthenticator
{
    /** Constructor omitted */

    public function supports(Request $request)
    {
        return 'app_login' === $request->attributes->get('_route')
            && $request->isMethod('POST');
    }

    public function getCredentials(Request $request)
    {
        $credentials = [
            'nomusuari' => $request->request->get('nomusuari'),
            'password' => $request->request->get('password'),
            'csrf_token' => $request->request->get('_csrf_token'),
        ];
        $request->getSession()->set(
            Security::LAST_USERNAME,
            $credentials['nomusuari']
        );

        return $credentials;
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        $token = new CsrfToken('authenticate', $credentials['csrf_token']);
        if (!$this->csrfTokenManager->isTokenValid($token)) {
            throw new InvalidCsrfTokenException();
        }

        $user = $userProvider->loadUserByUsername($credentials['nomusuari']);

        if (!$user) {
            // fail authentication with a custom error
            throw new CustomUserMessageAuthenticationException('Invalid user/password');
        }

        return $user;
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        $valid = $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
        return $valid;
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        $request->getSession()->set("user_username",$token->getUsername());

        return new RedirectResponse(
          $this->urlGenerator->generate("main")
        );
    }

    protected function getLoginUrl()
    {
        return $this->urlGenerator->generate('app_login');
    }
}

SessionAuthenticator(返回JSON,您可能需要进行调整):

class SessionAuthenticator extends AbstractGuardAuthenticator
{
    /**
     * Called on every request to decide if this authenticator should be
     * used for the request. Returning `false` will cause this authenticator
     * to be skipped.
     */
    public function supports(Request $request)
    {
        return $request->getSession()->has("user_username");
    }

    /**
     * Called on every request. Return whatever credentials you want to
     * be passed to getUser() as $credentials.
     */
    public function getCredentials(Request $request)
    {
        return $request->getSession()->get("user_username","");
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        if (null === $credentials) {
            // The token header was empty, authentication fails with HTTP Status
            // Code 401 "Unauthorized"
            return null;
        }

        // if a User is returned, checkCredentials() is called
        /*return $this->em->getRepository(User::class)
            ->findOneBy(['apiToken' => $credentials])
        ;*/
        return $userProvider->loadUserByUsername($credentials);
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        // Check credentials - e.g. make sure the password is valid.
        // In case of an API token, no credential check is needed.

        // Return `true` to cause authentication success
        return true;
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        // on success, let the request continue
        return null;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        $data = [
            // you may want to customize or obfuscate the message first
            'message' => strtr($exception->getMessageKey(), $exception->getMessageData())

            // or to translate this message
            // $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
        ];

        return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
    }

    /**
     * Called when authentication is needed, but it's not sent
     */
    public function start(Request $request, AuthenticationException $authException = null)
    {
        $data = [
            // you might translate this message
            'message' => 'Authentication Required'
        ];

        return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
    }

    public function supportsRememberMe()
    {
        return false;
    }
}

最后,我的security.yaml文件:

main:
            anonymous:
            stateless: true
            guard:
                entry_point: App\Security\FormAuthenticator
                authenticators:
                    - App\Security\SessionAuthenticator
                    - App\Security\FormAuthenticator

工作正常。我可以看到工具栏中的更改,角色已刷新。

希望对你有所帮助,

Esteve


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