Symfony 5:使用自定义用户实体进行 LDAP 身份验证

ben*_*.IT 4 ldap symfony symfony5

我想在 symfony 5 中实现以下身份验证场景:

  • 用户发送包含用户名和密码的登录表单,根据 LDAP 服务器进行身份验证
    • 如果针对 LDAP 服务器的身份验证成功:
      • 如果存在 my 的实例App\Entity\User,其用户名与 ldap 匹配条目相同,则从 ldap 服务器刷新其一些属性并返回该实体
      • 如果没有实例,则创建 my 的新实例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
Run Code Online (Sandbox Code Playgroud)

ben*_*.IT 5

我终于找到了一个很好的工作解决方案。缺少的部分是自定义用户提供程序。该用户提供者有责任根据 LDAP 对用户进行身份验证并返回匹配的App\Entity\User实体。这是在类getUserEntityCheckedFromLdap方法中完成的LdapUserProvider

App\Entity\User如果数据库中没有保存实例,自定义用户提供程序将实例化一个实例并将其保留。这是first user connection用例。

完整的代码可以在这个公共 github 存储库中找到

您将在下面找到我为使 LDAP 连接正常工作而遵循的详细步骤。

因此,让我们在 中声明自定义用户提供程序security.yaml

security.yaml:

    providers:
        ldap_user_provider:
            id: App\Security\LdapUserProvider
Run Code Online (Sandbox Code Playgroud)

现在,将其配置为服务,以在services.yaml. 请注意,由于我们要自动装配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)%'
Run Code Online (Sandbox Code Playgroud)

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'
Run Code Online (Sandbox Code Playgroud)

实现自定义用户提供程序 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);
        }
    }
Run Code Online (Sandbox Code Playgroud)

配置防火墙以使用我们的自定义用户提供程序:

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
Run Code Online (Sandbox Code Playgroud)

编写一个身份验证守卫:

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');
    }
}
Run Code Online (Sandbox Code Playgroud)

我的用户实体如下所示:

`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;
        }
    }
Run Code Online (Sandbox Code Playgroud)