用户角色何时刷新以及如何强制它?

时间:2012-12-10 10:02:22

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

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

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

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

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

9 个答案:

答案 0 :(得分:21)

因此,经过几天尝试找到可行的解决方案并为Symfony2用户邮件列表做出贡献之后,我终于找到了它。以下内容来自https://groups.google.com/d/topic/symfony2/NDBb4JN3mNc/discussion

的讨论

事实证明,有一个接口Symfony\Component\Security\Core\User\EquatableInterface并非用于比较对象标识,而是用于

  

测试两个对象在安全性和重新认证上下文中是否相同

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

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

以下是具体方法的示例:

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 :(得分:11)

在security.yml(或替代方案)中:

security:
    always_authenticate_before_granting: true

我生命中最简单的游戏。

答案 2 :(得分:7)

在Controller中,将角色添加到用户并保存到数据库后,只需调用:

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

答案 3 :(得分:4)

查看here,在always_authenticate_before_grantingtrue设为security.yml

答案 4 :(得分: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;
   }

诀窍是每次拨打setRoles时拨打loadByUsername ...希望有帮助

答案 5 :(得分:1)

解决方案是将订阅者挂在Doctrine postUpdate 事件上。如果更新的实体是User,与登录的用户相同,那么我使用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);
            }
        }
    }
}

答案 6 :(得分:0)

很抱歉,我无法回复评论,所以我重播提问。如果symfony安全新手试图在自定义密码身份验证中获得角色刷新工作,那么在函数authenticateToken中:

div{
display:table;
margin:1.5em auto 0 auto;
overflow:hidden;
border:1px solid #0a93cd;
border-radius:.5em;
width:50em;
padding:0;
}

div h2{
margin:0;
padding:.1em 0 .1em .5em;
padding:0 0 0.1em .5em;
text-align:center;
border:1px solid #0a93cd;
border-width:0px 0 1px 0 ;
background:#0a93cd;
 }

div h2 a{
color:#0a93cd;
color:#fff;
font:normal .8em arial;
font:normal 1em arial;
font:bold .8em arial;
}

div h2 a:hover{
color:#00006d;
}

div.top img{
width:100%;
display:block;
border-bottom:1px solid #0a93cd;
}

div.left img{
width:50%;
float:left;
border-right:2px solid #0a93cd;
}

div.right img{
width:50%;
float:right;
border-bottom:1px solid #0a93cd;
}

div p{padding:.5em 1em .5em 1em;}

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

答案 7 :(得分:0)

现在Symfony 4.1已有一段时间了,
然后我通过Manually Authenticating the User解决了这个问题。

答案 8 :(得分: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

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

HTH,

史蒂夫