Symfony 5:使用自定义用户实体的 ldap 身份验证

Symfony 5: ldap authentication with custom user entity

我想在 symfony 5 中实现以下身份验证方案:

我已经实施了一个保护身份验证器,它可以很好地针对 LDAP 服务器进行身份验证,但是它 return 给我 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

我终于找到了一个很好的工作解决方案。 丢失的部分是 custom user provider。 该用户提供者有责任根据 ldap 和 return 匹配的 App\Entity\User 实体对用户进行身份验证。这是在 LdapUserProvider class.

getUserEntityCheckedFromLdap 方法中完成的

如果数据库中没有保存 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');
    }
}

我的用户实体如下所示:

`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;
        }
    }