我想在symfony 5中实现以下身份验证方案:
App\Entity\User
的一个实例与ldap匹配条目的用户名相同,请从ldap服务器刷新其某些属性并返回该实体App\Entity\User
的新实例并返回我已经实现了一个保护身份验证器,可以很好地针对LDAP服务器进行身份验证,但是它向我返回了Symfony\Component\Ldap\Security\LdapUser
的实例,我不知道如何使用该对象与其他实体建立联系!
例如,假设我有一个具有Car
属性的owner
实体,该实体必须是对用户的引用。
我该如何处理?
这是我的security.yaml
文件的代码:
security:
encoders:
App\Entity\User:
algorithm: auto
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
my_ldap:
ldap:
service: Symfony\Component\Ldap\Ldap
base_dn: "%env(LDAP_BASE_DN)%"
search_dn: "%env(LDAP_SEARCH_DN)%"
search_password: "%env(LDAP_SEARCH_PASSWORD)%"
default_roles: ROLE_USER
uid_key: uid
extra_fields: ['mail']
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
lazy: true
provider: my_ldap
guard:
authenticators:
- App\Security\LdapFormAuthenticator
答案 0 :(得分:1)
我终于找到了一个很好的解决方案。
丢失的部分是custom user provider。
该用户提供者有责任根据ldap对用户进行身份验证,并返回匹配的App\Entity\User
实体。这是通过getUserEntityCheckedFromLdap
类的LdapUserProvider
方法完成的。
如果数据库中没有保存App\Entity\User
的实例,那么定制用户提供程序将实例化一个实例并将其持久化。这是first user connection
用例。
Full code is available in this public github repository。
您将在下面找到使ldap连接正常运行的详细步骤。
因此,让我们在security.yaml
中声明自定义用户提供程序。
security.yaml
:
providers:
ldap_user_provider:
id: App\Security\LdapUserProvider
现在,将其配置为服务,以在services.yaml
中传递一些ldap有用的字符串参数。
注意,由于我们将自动连接Symfony\Component\Ldap\Ldap
服务,因此我们也添加此服务配置:
services.yaml
:
#see https://symfony.com/doc/current/security/ldap.html
Symfony\Component\Ldap\Ldap:
arguments: ['@Symfony\Component\Ldap\Adapter\ExtLdap\Adapter']
Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
arguments:
- host: ldap
port: 389
# encryption: tls
options:
protocol_version: 3
referrals: false
App\Security\LdapUserProvider:
arguments:
$ldapBaseDn: '%env(LDAP_BASE_DN)%'
$ldapSearchDn: '%env(LDAP_SEARCH_DN)%'
$ldapSearchPassword: '%env(LDAP_SEARCH_PASSWORD)%'
$ldapSearchDnString: '%env(LDAP_SEARCH_DN_STRING)%'
请注意App\Security\LdapUserProvider
的参数来自环境变量。
.env
:
LDAP_URL=ldap://ldap:389
LDAP_BASE_DN=dc=mycorp,dc=com
LDAP_SEARCH_DN=cn=admin,dc=mycorp,dc=com
LDAP_SEARCH_PASSWORD=s3cr3tpassw0rd
LDAP_SEARCH_DN_STRING='uid=%s,ou=People,dc=mycorp,dc=com'
实施自定义用户提供程序:
App\Security\LdapUserProvider
:
<?php
namespace App\Security;
use App\Entity\User;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Ldap\Ldap;
use Symfony\Component\Ldap\LdapInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class LdapUserProvider implements UserProviderInterface
{
/**
* @var Ldap
*/
private $ldap;
/**
* @var EntityManager
*/
private $entityManager;
/**
* @var string
*/
private $ldapSearchDn;
/**
* @var string
*/
private $ldapSearchPassword;
/**
* @var string
*/
private $ldapBaseDn;
/**
* @var string
*/
private $ldapSearchDnString;
public function __construct(EntityManagerInterface $entityManager, Ldap $ldap, string $ldapSearchDn, string $ldapSearchPassword, string $ldapBaseDn, string $ldapSearchDnString)
{
$this->ldap = $ldap;
$this->entityManager = $entityManager;
$this->ldapSearchDn = $ldapSearchDn;
$this->ldapSearchPassword = $ldapSearchPassword;
$this->ldapBaseDn = $ldapBaseDn;
$this->ldapSearchDnString = $ldapSearchDnString;
}
/**
* @param string $username
* @return UserInterface|void
* @see getUserEntityCheckedFromLdap(string $username, string $password)
*/
public function loadUserByUsername($username)
{
// must be present because UserProviders must implement UserProviderInterface
}
/**
* search user against ldap and returns the matching App\Entity\User. The $user entity will be created if not exists.
* @param string $username
* @param string $password
* @return User|object|null
*/
public function getUserEntityCheckedFromLdap(string $username, string $password)
{
$this->ldap->bind(sprintf($this->ldapSearchDnString, $username), $password);
$username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_FILTER);
$search = $this->ldap->query($this->ldapBaseDn, 'uid=' . $username);
$entries = $search->execute();
$count = count($entries);
if (!$count) {
throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username));
}
if ($count > 1) {
throw new UsernameNotFoundException('More than one user found');
}
$ldapEntry = $entries[0];
$userRepository = $this->entityManager->getRepository('App\Entity\User');
if (!$user = $userRepository->findOneBy(['userName' => $username])) {
$user = new User();
$user->setUserName($username);
$user->setEmail($ldapEntry->getAttribute('mail')[0]);
$this->entityManager->persist($user);
$this->entityManager->flush();
}
return $user;
}
/**
* Refreshes the user after being reloaded from the session.
*
* When a user is logged in, at the beginning of each request, the
* User object is loaded from the session and then this method is
* called. Your job is to make sure the user's data is still fresh by,
* for example, re-querying for fresh User data.
*
* If your firewall is "stateless: true" (for a pure API), this
* method is not called.
*
* @return UserInterface
*/
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user)));
}
return $user;
// Return a User object after making sure its data is "fresh".
// Or throw a UsernameNotFoundException if the user no longer exists.
throw new \Exception('TODO: fill in refreshUser() inside ' . __FILE__);
}
/**
* Tells Symfony to use this provider for this User class.
*/
public function supportsClass($class)
{
return User::class === $class || is_subclass_of($class, User::class);
}
}
配置防火墙以使用我们的自定义用户提供程序:
security.yaml
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
lazy: true
provider: ldap_user_provider
logout:
path: app_logout
guard:
authenticators:
- App\Security\LdapFormAuthenticator
写一个身份验证保护:
App\SecurityLdapFormAuthenticator
:
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class LdapFormAuthenticator extends AbstractFormLoginAuthenticator
{
use TargetPathTrait;
private $urlGenerator;
private $csrfTokenManager;
public function __construct(UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager)
{
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
}
public function supports(Request $request)
{
return 'app_login' === $request->attributes->get('_route') && $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
$credentials = [
'username' => $request->request->get('_username'),
'password' => $request->request->get('_password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['username']
);
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->getUserEntityCheckedFromLdap($credentials['username'], $credentials['password']);
if (!$user) {
throw new CustomUserMessageAuthenticationException('Username could not be found.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
//in this scenario, this method is by-passed since user authentication need to be managed before in getUser method.
return true;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$request->getSession()->getFlashBag()->add('info', 'connected!');
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('app_homepage'));
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate('app_login');
}
}
My user entity looks like this:
`App\Entity\User`:
<?php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @ORM\Entity(repositoryClass=UserRepository::class)
*/
class User implements UserInterface
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=180, unique=true)
*/
private $email;
/**
* @var string The hashed password
* @ORM\Column(type="string")
*/
private $password = 'password is not managed in entity but in ldap';
/**
* @ORM\Column(type="string", length=255)
*/
private $userName;
/**
* @ORM\Column(type="json")
*/
private $roles = [];
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUsername(): string
{
return (string) $this->email;
}
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* @see UserInterface
*/
public function getPassword(): string
{
return (string) $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* @see UserInterface
*/
public function getSalt()
{
// not needed when using the "bcrypt" algorithm in security.yaml
}
/**
* @see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function setUserName(string $userName): self
{
$this->userName = $userName;
return $this;
}
}