【问题标题】:When are user roles refreshed and how to force it?用户角色何时刷新以及如何强制刷新?
【发布时间】:2012-12-10 10:02:22
【问题描述】:

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

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

我的第一次尝试是从Symfony\Component\Security\Core\User\UserInterface 加载并返回getRoles() 方法中的所有用户角色。起初,它看起来很有效。但在深入研究之后,我注意到这些角色仅在用户登录时才会刷新。这意味着如果我授予或撤销用户的角色,他必须注销并重新登录才能使更改生效。但是,如果我撤销用户的安全角色,我希望立即应用它,这样我就不能接受这种行为。

我希望 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 在每个请求上刷新用户角色?

【问题讨论】:

    标签: security symfony roles symfony-2.1 symfony-security


    【解决方案1】:

    所以在尝试了几天试图找到一个可行的解决方案并为 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 注释,防火墙中没有必需的角色等),则实际上并没有完成刷新,并且用户保持在“未验证”状态。

    一旦您点击执行任何类型的授权检查的页面,用户角色就会被刷新,并且分析器工具栏会再次显示带有绿点和“已验证:是”的用户。

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

    【讨论】:

    • +1 用于“确保将角色存储在用户对象的字段中”提示,这救了我的命
    • 别忘了实现 \Serializable 接口并包含 id、salt 和 isActive。由于您还需要检查角色是否有变化,因此也将其添加到可序列化数据中。
    • 太棒了! 我已经实现了 FOSUserBundle,它为我提供了我的用户实体扩展的 FOS\UserBundle\Model\User。我刚刚实现了EquatableInterface,它就像一个魅力(因为FOS的用户已经完成了序列化)。
    • 当您实现EquatableInterface 时,symfony 会跳过所有其他检查用户是否已更改的检查(请参阅github.com/symfony/security-core/blob/master/Authentication/…)。所以我想你必须在isEqualTo 函数中自己做所有这些事情?
    • 你可以将主要部分写在一行return count($this->getRoles()) == count($user->getRoles()) && count(array_diff($this->getRoles(), $user->getRoles())) == 0;
    【解决方案2】:

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

    security:
        always_authenticate_before_granting: true
    

    我一生中最简单的游戏。

    【讨论】:

    • 别忘了重建bootstrap.php.cache。 symfony3.2
    【解决方案3】:

    在控制器中,为用户添加角色并保存到数据库后,只需调用:

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

    【讨论】:

    • 非常适合我,但如果你想避免在较新版本的 Symfony 中出现弃用通知,请使用 security.token_storage 而不是 security.context :)
    • 在服务上,您可以将其添加到事件 'kernel.controller' 以强制其他已登录的用户刷新权限角色
    【解决方案4】:

    看看here,将always_authenticate_before_granting 设置为true security.yml

    【讨论】:

      【解决方案5】:

      我通过实现自己的 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 的安全上下文中的内容?
      • 用户对象在令牌中设置并在用于令牌的持久层(会话/cookies/bdd)中检索。如果您没有在每次刷新用户时专门 refreshRoles,则安全上下文适用于令牌角色(与 PHP 会话一起持久)。让我们看看 DaoAuthenticationProvider::retirieveUser 方法。无论您使用何种方式来管理您的用户,您都必须调整 UserProvider::loadByUsername 方法以刷新每个请求的角色..
      • 是的,我明白你的意思:用户对象存储在令牌中,令牌(在我的情况下)存储在用户的会话中。但这不正是refreshUser(UserInterface $user) 方法的用途吗?我的 UserProvider 从数据库加载并返回一个新用户(包括角色!),但他们仍然没有在令牌中刷新。
      • 它对我有用(在 refreshUser 或 loadUserByUsername 中明确使用 setRoles)...请发布您的 refreshUser 方法,它可能有助于找出答案。
      • 除非 roles 是您的 User 对象的数组字段(DB 列),否则它不会在您的 loadUserByUsername 方法中刷新...
      【解决方案6】:

      解决方案是将订阅者挂在 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);
                  }
              }
          }
      }
      

      【讨论】:

        【解决方案7】:

        抱歉,我无法在评论中回复,所以我重播了问题。如果 symfony 安全领域的新人尝试在自定义密码身份验证中进行角色刷新工作,那么在函数 authenticateToken 中:

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

        并且不要从 DB/LDAP 或任何地方检查密码。如果用户进入系统,那么 $token 中只是用户名,没有角色。

        【讨论】:

          【解决方案8】:

          我一直在为 Symfony4 争取这个,我想我终于找到了一个解决方案。

          问题是,就我而言,角色取决于用户正在使用的“公司”。它可能是一家公司的 CEO,但另一家公司的运营商,菜单、权限等取决于公司。切换公司时,用户不得重新登录。

          最后我做了以下事情:

          • 将防火墙设置为无状态。
          • 在 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
          

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

          HTH,

          埃斯特夫

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2016-05-18
            • 2021-03-16
            • 2013-02-26
            • 2012-12-15
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2010-11-16
            相关资源
            最近更新 更多