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

时间:2020-10-09 12:42:55

标签: symfony ldap symfony5

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

  • 用户发送带有用户名和密码的登录表单,对LDAP服务器进行身份验证
    • 如果针对LDAP服务器的身份验证成功:
      • 如果我的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

1 个答案:

答案 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;
        }
    }