在 Symfony 4 中将身份验证与您的用户实体分离?
Decouple authentication from your user entity in Symfony 4?
在使用同一实体维护员工和跟踪我网站的登录时遇到一些问题后,我发现了一个 post here,它讨论了与安全用户分离的问题。不幸的是,我觉得 post 在如何完全实施它方面留下了很多缺失(而且似乎我不是唯一的)。首先为什么我需要这样做以及我将如何在 Symfony 4 中实现它?
为什么我需要这样做?
这里解释的比较详细here
...this also comes with side effects:
You will end up with this Entity in your session Developers tend to
also use this entity in forms Session Entities If you end up with
Entities in your session, you will get synchronization issues. If you
update your entity, that means your session entity won't be updated as
it's not from the database. In order to solve this issue, you can
merge the entity back into the entity manager each request.
While this solves one of the problems, another common issue is the
(un)serialization. Eventually your User Entity will get relations to
other objects and this comes with several side-effects:
Relations will be serialized as well If a relation is lazy loaded
(standard setting), it will try to serialize the Proxy which contains
a connection. This will spew some errors on your screen as the
connection cannot be serialized. Oh and don't even think about
changing your Entity such as adding fields, this will cause
unserialization issues with incomplete objects because of missing
properties. This case is triggered for every authenticated user.
基本上问题是,如果您使用相同的实体进行身份验证和处理 users/employees/clients/etc。你会遇到这样的问题,当你更改实体的 属性 时,它会导致经过身份验证的用户与数据库中的内容不同步 - 导致角色不正确的问题,用户突然被迫注销(感谢 ),或其他问题,具体取决于用户 class 在您的系统中的使用方式。
我该如何解决这个问题?
假设:我假设你有一个 'User' 至少有用户名、密码和角色的实体
为了解决这个问题,我们需要制作几个单独的服务,它们将充当用户实体和用户之间的桥梁以进行身份验证。
这首先是创建一个安全用户,它利用用户 class
的字段
安全用户 /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()
{
}
}
有了这个,我们从我们的用户实体中提取记录 - 这意味着我们不需要单独的 table 来存储用户信息,而且我们已经将身份验证用户与我们的实体分离 -这意味着对实体的更改现在不会直接影响 SecurityUser。
为了让 Symfony 通过这个 SecurityUser class 进行身份验证,我们需要创建一个提供者:
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);
}
}
该服务基本上会要求查询数据库中的用户名,然后查询相关用户名的角色。如果找不到用户名,则会产生错误。然后它 return 将一个 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 和线程的值非常主观,具体取决于您的系统。您可以查看我的 post ,它可以帮助您获得适合您系统的正确值
此时您的安全性应该可以正常工作,并且您已经与用户实体完全分离 - 恭喜!
其他感兴趣的相关领域
既然你已经有了这个,也许你应该添加一些代码,这样你的用户会话在空闲了这么长时间后就会被销毁。为此,请查看我的回答 here.
在使用同一实体维护员工和跟踪我网站的登录时遇到一些问题后,我发现了一个 post here,它讨论了与安全用户分离的问题。不幸的是,我觉得 post 在如何完全实施它方面留下了很多缺失(而且似乎我不是唯一的)。首先为什么我需要这样做以及我将如何在 Symfony 4 中实现它?
为什么我需要这样做?
这里解释的比较详细here
...this also comes with side effects: You will end up with this Entity in your session Developers tend to also use this entity in forms Session Entities If you end up with Entities in your session, you will get synchronization issues. If you update your entity, that means your session entity won't be updated as it's not from the database. In order to solve this issue, you can merge the entity back into the entity manager each request.
While this solves one of the problems, another common issue is the (un)serialization. Eventually your User Entity will get relations to other objects and this comes with several side-effects:
Relations will be serialized as well If a relation is lazy loaded (standard setting), it will try to serialize the Proxy which contains a connection. This will spew some errors on your screen as the connection cannot be serialized. Oh and don't even think about changing your Entity such as adding fields, this will cause unserialization issues with incomplete objects because of missing properties. This case is triggered for every authenticated user.
基本上问题是,如果您使用相同的实体进行身份验证和处理 users/employees/clients/etc。你会遇到这样的问题,当你更改实体的 属性 时,它会导致经过身份验证的用户与数据库中的内容不同步 - 导致角色不正确的问题,用户突然被迫注销(感谢
我该如何解决这个问题?
假设:我假设你有一个 'User' 至少有用户名、密码和角色的实体
为了解决这个问题,我们需要制作几个单独的服务,它们将充当用户实体和用户之间的桥梁以进行身份验证。
这首先是创建一个安全用户,它利用用户 class
的字段安全用户 /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()
{
}
}
有了这个,我们从我们的用户实体中提取记录 - 这意味着我们不需要单独的 table 来存储用户信息,而且我们已经将身份验证用户与我们的实体分离 -这意味着对实体的更改现在不会直接影响 SecurityUser。
为了让 Symfony 通过这个 SecurityUser class 进行身份验证,我们需要创建一个提供者:
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);
}
}
该服务基本上会要求查询数据库中的用户名,然后查询相关用户名的角色。如果找不到用户名,则会产生错误。然后它 return 将一个 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 和线程的值非常主观,具体取决于您的系统。您可以查看我的 post
此时您的安全性应该可以正常工作,并且您已经与用户实体完全分离 - 恭喜!
其他感兴趣的相关领域
既然你已经有了这个,也许你应该添加一些代码,这样你的用户会话在空闲了这么长时间后就会被销毁。为此,请查看我的回答 here.