将身份验证与您在Symfony 4中的用户实体分离?

时间:2018-09-28 17:08:06

标签: php doctrine symfony4

在使用同一个实体来维护员工和跟踪我的网站的登录时遇到了一些问题之后,我发现了一条帖子here,其中谈到了将安全用户解耦。不幸的是,我觉得该职位在如何完全实施方面遗漏了很多(而且似乎我并不是唯一的一位)。首先,为什么我需要这样做,以及如何在Symfony 4中实现它?

1 个答案:

答案 0 :(得分:1)

我为什么需要这样做?

对此进行了更详细的说明here

  

...这还带有副作用:   开发人员倾向于在会话中使用该实体   也可以在表单会话实体中使用此实体   在会话中的实体,您会遇到同步问题。如果你   更新您的实体,这意味着您的会话实体不会更新为   它不是来自数据库。为了解决这个问题,您可以   每个请求将实体合并回实体管理器。

     

虽然可以解决其中一个问题,但另一个常见的问题是   (非)序列化。最终,您的用户实体将与   其他对象,并且有一些副作用:

     

关系也将被序列化如果一个关系被延迟加载   (标准设置),它将尝试序列化包含以下内容的代理   连接。这会在屏幕上产生一些错误,因为   连接无法序列化。哦,甚至不用考虑   更改您的实体(例如添加字段),这将导致   由于丢失而导致对象不完整的序列化问题   属性。每个经过身份验证的用户都会触发这种情况。

基本上,问题是如果您使用相同的实体进行身份验证以及与用户/员工/客户/等等打交道。您将遇到一个问题,即当您更改实体的属性时,它将导致已通过身份验证的用户与数据库中的内容不同步-导致角色不正确的问题,用户突然被迫注销(谢谢)到logout_on_user_change setting),还是其他问题取决于系统中用户类的使用方式。

我该如何解决?

假设:我假设您有一个“用户”实体,该实体至少具有用户名,密码和角色

为了解决此问题,我们需要提供几个单独的服务,这些服务将充当User实体和User之间的桥梁以进行身份​​验证。

这首先是创建一个利用用户类字段的安全用户

SecurityUser /app/Security/SecurityUser.php

<?php

namespace App\Security;

use App\Entity\User;
use Symfony\Component\Security\Core\User\UserInterface;

class SecurityUser implements UserInterface, \Serializable
{
    private $username;
    private $password;
    private $roles;

    public function __construct(User $user)
    {
        $this->username = $user->getUsername();
        $this->password = $user->getPassword();
        $this->roles = $user->getRoles();
    }

    public function getUsername(): ?string
    {
        return $this->username;
    }

    public function getPassword(): ?string
    {
        return $this->password;
    }

    public function getSalt()
    {
        // you *may* need a real salt depending on your encoder
        // see section on salt below
        return null;
    }

    /** @see \Serializable::serialize() */
    public function serialize()
    {
        return serialize(array(
            $this->username,
            $this->password,
            // Should only be set if your encoder uses a salt i.e. PBKDF2
            // This example uses Argon2i
            // $this->salt,
        ));
    }

    /** @see \Serializable::unserialize() */
    public function unserialize($serialized)
    {
        list (
            $this->username,
            $this->password,
            // Should only be set if your encoder uses a salt i.e. PBKDF2
            // This example uses Argon2i
            // $this->salt
            ) = unserialize($serialized, array('allowed_classes' => false));
    }

    public function getRoles()
    {
        return $this->roles;
    }

    public function eraseCredentials()
    {
    }
}

这样,我们从User实体中提取记录-这意味着我们不需要单独的表来存储用户信息,但是我们已经将身份验证用户与Entity分离了-意味着更改为Entity现在不会直接影响SecurityUser。

为了使Symfony使用此SecurityUser类进行身份验证,我们将需要创建一个提供程序:

SecurityUserProvider / app / Security / SecurityUserProvider

<?php

namespace App\Security;

use App\Repository\UserRepository;
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;

class SecurityUserProvider implements UserProviderInterface
{
    private $userRepository;

    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function loadUserByUsername($username)
    {
        return $this->fetchUser($username);
    }

    public function refreshUser(UserInterface $user)
    {
        if (!$user instanceof SecurityUser) {
            throw new UnsupportedUserException(
                sprintf('Instances of "%s" are not supported.', get_class($user))
            );
        }

        $username = $user->getUsername();

        $this->logger->info('Username (Refresh): '.$username);

        return $this->fetchUser($username);
    }

    public function supportsClass($class)
    {
        return SecurityUser::class === $class;
    }

    private function fetchUser($username)
    {
        if (null === ($user = $this->userRepository->findOneBy(['username' => $username]))) {
            throw new UsernameNotFoundException(
                sprintf('Username "%s" does not exist.', $username)
            );
        }

        return new SecurityUser($user);
    }
}

该服务基本上会在数据库中询问用户名,然后询问相关用户名的角色。如果找不到用户名,则会创建错误。然后,它将SecurityUser对象返回给Symfony进行身份验证。

现在我们需要告诉Symfony使用该对象

Securty.yaml /app/config/packages/security.yaml

security:
    ...
    providers:
        db_provider:
            id: App\Security\SecurityUserProvider

名称“ db_provider”无关紧要-您可以使用任何您想要的名称。此名称仅用于将提供程序映射到防火墙。如何配置防火墙超出了本文档的范围,请参见here,以获取有关其的非常好的文档。无论如何,如果出于某种原因您对我的模样感到好奇(尽管我不会去解释它):

security:
    ...
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            pattern:    ^/
            anonymous: ~
            provider: db_provider
            form_login:
                login_path: login
                check_path: login
            logout:
                path:   /logout
                target: /
                invalidate_session: true

    access_control:
        - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/, roles: ROLE_USER }

最后,我们需要配置编码器,以便我们可以加密密码。

security:
    ...
    encoders:
        App\Security\SecurityUser:
            algorithm: argon2i
            memory_cost: 102400
            time_cost: 3
            threads: 4

旁注(题外): 请注意,我正在使用Argon2i。 memory_cost,time_cost和threads的值非常主观,具体取决于您的系统。您可以看到我的帖子here,它可以帮助您获取系统的正确值

在这一点上,您的安全性应该正常工作,并且您已经完全与用户实体脱钩了-恭喜!

其他相关领域

现在有了这个,也许您应该添加一些代码,以便您的用户会话在空闲了很长时间后将被销毁。为此,请查看我的答案here