Symfony LDAP с настраиваемой сущностью пользователя и автоматическим созданием пользователя БД

Я пытаюсь реализовать простую аутентификацию LDAP в своем приложении Symfony.

Пользователь должен сначала пройти аутентификацию в LDAP, при этом из базы данных должна быть возвращена пользовательская сущность пользователя.

Если пользователя нет в базе данных, но он может быть успешно аутентифицирован, я хочу создать пользователя.

За исключением автоматического создания пользователя в базе, пока работает.

providers:
        users_db:
            entity:
                # the class of the entity that represents users
                class: 'App\Entity\User'
                # the property to query by - e.g. email, username, etc
                property: 'username'
        users_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
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: users_db
            http_basic_ldap:
                service: Symfony\Component\Ldap\Ldap
                dn_string: '%env(LDAP_DN_STRING)%'

С приведенной выше конфигурацией аутентификация работает с LDAP, но пользователь приходит из БД. Поэтому, если я не создал соответствующего пользователя, попытка входа не сработает.

Я пробовал автоматическое создание пользователей с помощью UserChecker , UserProvider и LoginEventListener (прослушивает событие onAuthenticationSuccess), к сожалению, безуспешно.

Событие onAuthenticationSuccess вызывается только после успешной аутентификации и поэтому не может использоваться в конфигурации, описанной выше, поскольку провайдер users_db (пока) не содержит пользователя, даже если базовая аутентификация LDAP работает.

Затем я попробовал это с помощью UserChecker и сетевого провайдера [users_ldap, users_db]. Это также выполняется, но тогда я получаю уже не пользовательский объект, а пользователя LDAP. Поэтому я попытался создать свой собственный UserProvider, но, к сожалению, безуспешно.

Если кто-нибудь знает хороший способ сделать это, я был бы признателен за ответ или короткий комментарий. Спасибо!

у меня был аналогичный проект, где мне не нужно было хранить пароли пользователей, только имя пользователя, адрес электронной почты и имя (данные, полученные из ldap, когда аутентификация прошла успешно). Используется пользовательский аутентификатор, где, когда пользователь отправляет форму (с именем пользователя и паролем), я проверяю их на сервере ldap, в случае успеха проверяю мою базу данных, если имя пользователя (из формы) существует, если не создается новый объект с данные из ответа ldap.

benkov 09.11.2022 10:11

Большое спасибо за предложение. Я сейчас сделал это аналогично https://stackoverflow.com/a/72138096/2117102. Это было немного сложно, но я думаю, что его подход имеет смысл. Я приложу ответ с кодом, как только найду время.

pnine 11.11.2022 14:08
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Symfony Station Communiqué - 7 июля 2023 г
Symfony Station Communiqué - 7 июля 2023 г
Это коммюнике первоначально появилось на Symfony Station .
Symfony Station Communiqué - 17 февраля 2023 г
Symfony Station Communiqué - 17 февраля 2023 г
Это коммюнике первоначально появилось на Symfony Station , вашем источнике передовых новостей Symfony, PHP и кибербезопасности.
Управление ответами api для исключений на Symfony с помощью KernelEvents
Управление ответами api для исключений на Symfony с помощью KernelEvents
Много раз при создании api нам нужно возвращать клиентам разные ответы в зависимости от возникшего исключения.
0
2
80
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я сталкивался с вашей проблемой раньше, мне не удалось автоматически сохранить их информацию в моей локальной базе данных, поэтому я выполняю следующие шаги:

  1. вход с использованием cas auth и Ldap в качестве провайдера пользователя.
  2. перенаправить их в форму и получить их в полях, например:
 <div>
        {{ form_row(user_form.uid, { label: 'UID :*',
            required: 'true',
            attr: {
               value : app.user.uid,
               readonly: true
            }            
         }) }}
 </div>
  1. После отправки формы их роли изменятся с «ROLE_VISIT» на «ROLE_USER», и вся информация LDAP будет сохранена в моей локальной базе данных.
  2. вы можете сохранить некоторые данные в частном порядке следующим образом:
<div class = "invisible" >
   {{ form_row(user_form.genre,{
      attr: { value : app.user.supannCivilite }
   }) }}
</div>

Я использую этот пакет для LDAP, поэтому, пожалуйста, дайте мне знать, если вам нужна помощь!

Кроме того, вы можете взглянуть на мою конфигурацию безопасности, как показано ниже, надеюсь, что это будет полезно:

    providers:
        chain_provider:
            chain:
                providers: [in_memory, database, ldap]
        in_memory:
            memory:
                users:
                    __NO_USER__:
                        password:
                        roles: ROLE_ANON
        database:
            entity:
                class: App\Entity\User
                property: uid

        ldap:
            id: ldap_user_provider

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        l3_firewall:
            pattern: ^/
            provider: chain_provider
            security: true
            guard:
                authenticators:
                    - cas.security.authentication.authenticator
            logout:
                path: /logout
                success_handler: authentication_handler
                invalidate_session: false
            access_denied_handler: App\EventListener\AccessDeniedListener
        main:
            pattern: ^/
            security: true
            lazy: true
            provider: chain_provider
            guard:
                authenticators:
                    - cas.security.authentication.authenticator

Большое спасибо за предложение и пример кода. Однако сейчас я решил это аналогично этому: https://stackoverflow.com/a/72138096/2117102.

pnine 11.11.2022 14:10
Ответ принят как подходящий

Благодаря этому ответу теперь у меня все работает.

конфиг/services.yaml


services:

  App\Security\UserProvider:
    arguments:
      $em: '@Doctrine\ORM\EntityManagerInterface'
      $ldap: '@Symfony\Component\Ldap\Ldap'
      $baseDn: "%env(LDAP_BASE_DN)%"
      $searchDn: "%env(LDAP_SEARCH_DN)%"
      $searchPassword: "%env(LDAP_SEARCH_PASSWORD)%"
      $defaultRoles: ["ROLE_USER"]
      $uidKey: "uid"
      $extraFields: []

  App\EventListener\LoginListener:
    arguments:
      - "@doctrine.orm.entity_manager"

конфигурация/пакеты/security.yml

security:
    enable_authenticator_manager: true
    password_hashers:
        App\Entity\User: 'auto'
    providers:
        users:
            id: App\Security\UserProvider
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: users
            stateless: false
            http_basic_ldap:
                service: Symfony\Component\Ldap\Ldap
                dn_string: 'uid = {username},ou=accounts,dc=example,dc=com'
            

src/Безопасность/UserProvider.php

<?php

namespace App\Security;

use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Ldap\Security\LdapUserProvider;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Ldap\Ldap;
use Symfony\Component\Ldap\Security\LdapUser;

class UserProvider extends LdapUserProvider
{
  private $em;

  public function __construct(EntityManagerInterface $em, Ldap $ldap, string $baseDn, string $searchDn = null, string $searchPassword = null, array $defaultRoles = [], string $uidKey = null, string $filter = null, string $passwordAttribute = null, array $extraFields = [])
  {
    parent::__construct($ldap, $baseDn, $searchDn, $searchPassword, $defaultRoles, $uidKey, $filter, $passwordAttribute, $extraFields);
    $this->em = $em;
  }

  /**
   * Refreshes the user after being reloaded from the session.
   *
   * @return UserInterface
   */
  public function refreshUser(UserInterface $user)
  {
    if (!$user instanceof User) {
      throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
    }
    $refreshUser = $this->em->getRepository(User::class)->findOneBy(['username' => $user->getUserIdentifier()]);

    return $refreshUser;
  }

  /**
   * Tells Symfony to use this provider for this User class.
   */
  public function supportsClass(string $class): bool
  {
    return User::class === $class || is_subclass_of($class, User::class) || LdapUser::class === $class || is_subclass_of($class, LdapUser::class);
  }
}

src/EventListener/LoginListener.php

<?php

namespace App\EventListener;

use App\Entity\User;
use Doctrine\ORM\EntityManager;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Ldap\Security\LdapUser;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

class LoginListener implements EventSubscriberInterface
{

  private $em;
  private $tokenStorage;

  function __construct(EntityManager $em, TokenStorageInterface $tokenStorage)
  {
    $this->em = $em;
    $this->tokenStorage = $tokenStorage;
  }

  public static function getSubscribedEvents(): array
  {
    return [LoginSuccessEvent::class => 'onLoginSuccess'];
  }

  public function onLoginSuccess(LoginSuccessEvent $loginSuccessEvent)
  {

    $ldapUser = $loginSuccessEvent->getAuthenticatedToken()->getUser();
    if (!($ldapUser instanceof LdapUser)) {
      return;
    }

    $localUser = $this->em->getRepository(User::class)->findOneBy(['username' => $ldapUser->getUserIdentifier()]);

    if (!$localUser) {
      // No local user found in database -> create new user
      $localUser = new User();
      $localUser->setUsername($ldapUser->getUserIdentifier());
    }
    // We don't store user passwords -> generate random token
    $rmdBytes = random_bytes(32);
    $localUser->setPassword($rmdBytes);

    $this->em->persist($localUser);
    $this->em->flush();

    // Login user
    $token = new UsernamePasswordToken($localUser, $rmdBytes, 'main', $localUser->getRoles());
    $this->tokenStorage->setToken($token);
  }
}

Я также реализовал EquatableInterface в объекте User, как это было предложено в упомянутом stackoverflow.

Другие вопросы по теме