簡體   English   中英

Symfony 5.1:LDAP 使用實體用戶提供程序進行身份驗證

[英]Symfony 5.1: LDAP Authentication with Entity User Provider

我正在使用 Symfony 5.1 並嘗試實現 LDAP 身份驗證,而用戶屬性(用戶名、角色等)存儲在 MySQL 數據庫中。 因此,我為 Doctrine 添加了一個用戶實體,並配置了與文檔對應的 services.yml 和 security.yml。

我還使用 Maker Bundle 生成了一個LoginFormAuthenticator ,它似乎使用了 Guard Authenticator 模塊。

當我嘗試登錄時,它看起來好像沒有做任何與 LDAP 相關的事情。 我還用 tcpdump 收聽了 TCP 包,沒有看到到 LDAP 服務器的任何流量。

這是我的代碼:

services.yml

services:
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.

    App\:
        resource: '../src/*'
        exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'

    App\Controller\:
        resource: '../src/Controller'
        tags: ['controller.service_arguments']

    Symfony\Component\Ldap\Ldap:
        arguments: ['@Symfony\Component\Ldap\Adapter\ExtLdap\Adapter']
    Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
        arguments:
            -   host: <ldap-IP>
                port: 389
                options:
                    protocol_version: 3
                    referrals: false

security.yml

security:
    encoders:
        App\Entity\User:
            algorithm: auto

        app_user_provider:
            entity:
                class: App\Entity\User
                property: email

        my_ldap:
            ldap:
                service: Symfony\Component\Ldap\Ldap
                base_dn: "<base_dn>"
                search_dn: "<search_dn>"
                search_password: "<password>"
                default_roles: ROLE_USER
                uid_key: sAMAccountName

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: true
            lazy: true
            provider: my_ldap

            form_login_ldap:
                login_path: login
                check_path: login
                service: Symfony\Component\Ldap\Ldap
                dn_string: 'uid={username},OU=Test,DC=domain,DC=domain'

            guard:
                authenticators:
                    - App\Security\LoginFormAuthenticator
            logout:
                path: app_logout
                # where to redirect after logout
                target: index

    access_control:
        - { path: ^/$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/profile, roles: ROLE_USER }

LoginFormAuthenticator ,我想問題出在checkCredentials function 中。 我找到了LdapBindAuthenticationProvider class,其目的似乎正是針對 LDAP 進行此類用戶憑據檢查,但我完全不確定我必須如何去做:

<?php

namespace App\Security;

use Psr\Log\LoggerInterface;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
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\Encoder\UserPasswordEncoderInterface;
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\Guard\PasswordAuthenticatedInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
use Symfony\Component\Ldap\Ldap;
use Symfony\Component\Security\Core\Authentication\Provider\LdapBindAuthenticationProvider;

class LoginFormAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface
{
    use TargetPathTrait;

    public const LOGIN_ROUTE = 'app_login';

    private $logger;
    private $entityManager;
    private $urlGenerator;
    private $csrfTokenManager;
    private $passwordEncoder;
    private $ldap;

    public function __construct(LoggerInterface $logger, EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder, Ldap $ldap, LdapBindAuthenticationProvider $form_login_ldap)
    {
        $this->logger = $logger;
        $this->entityManager = $entityManager;
        $this->urlGenerator = $urlGenerator;
        $this->csrfTokenManager = $csrfTokenManager;
        $this->passwordEncoder = $passwordEncoder;
        $this->ldap = $ldap;
    }

    public function supports(Request $request): ?bool
    {
        return self::LOGIN_ROUTE === $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 = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['username']]);

        if (!$user) {
            // user not found in db, but may exist in ldap:
            $user = $userProvider->loadUserByUsername($credentials['username']);
            if (!$user) {
                // user simply doesn't exist
                throw new CustomUserMessageAuthenticationException('Email could not be found.');
            } else {
                // user never logged in before, create user in DB and proceed...
                // TODO
            }
        }

        return $user;
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        // TODO: how to use the LdapBindAuthenticationProvider here to check the users credentials agains LDAP?
        return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
    }

    /**
     * Used to upgrade (rehash) the user's password automatically over time.
     */
    public function getPassword($credentials): ?string
    {
        return $credentials['password'];
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
            return new RedirectResponse($targetPath);
        }

        // For example : return new RedirectResponse($this->urlGenerator->generate('some_route'));
        throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
    }

    protected function getLoginUrl()
    {
        return $this->urlGenerator->generate(self::LOGIN_ROUTE);
    }
}

不幸的是,我沒有找到任何示例代碼。

更新:

感謝 T. van den Berg 的回答,我終於設法讓身份驗證部分正常工作。 我從 security.yml 中刪除了 LoginFormAuthenticator Guard,並稍微調整了 form_login_ldap。

security:
    encoders:
        App\Entity\User:
            algorithm: auto

    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email

        my_ldap:
            ldap:
                service: Symfony\Component\Ldap\Ldap
                base_dn: '<baseDN>'
                search_dn: '<bindDN>'
                search_password: '<bindDN password>'
                default_roles: ['ROLE_USER']

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        main:
            anonymous: true
            lazy: true
            provider: my_ldap

            form_login_ldap:
                login_path: app_login
                check_path: app_login
                service: Symfony\Component\Ldap\Ldap
                dn_string: '<baseDN>'
                query_string: '(sAMAccountName={username})'
                search_dn: '<bindDN>'
                search_password: '<bindDN password>'

            logout:
                path: app_logout
                target: index

    access_control:
        - { path: ^/$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/profile, roles: ROLE_USER }

It is now using the LDAPUserProvider to use the LDAP service user (bind DN) to fetch the user LDAP object by its login name (sAMAccountName) and then in a second request use the distinguished name (DN) of this LDAP object to make another authentication使用提供的密碼針對 LDAP 服務器。 到目前為止很好。

唯一缺少的是數據庫存儲的用戶實體。 我的想法如下:

  • 登錄表單已提交
  • 在數據庫中搜索具有提供的用戶名的用戶實體
  • 如果未找到用戶實體,請使用 LDAPUserProvider 向 LDAP 詢問用戶名
  • 如果 LDAP 中存在用戶,則在數據庫中創建用戶實體
  • 使用提供的密碼針對 LDAP 對用戶進行身份驗證

密碼未保存在數據庫中,但 LDAP 中沒有其他應用程序特定信息(例如上次活動)。

有誰知道如何實現這個?

如果您想在 LDAP 用戶登錄后將其保存到本地數據庫,則可以使用此捆綁包ldaptools/ldaptools-bundle (or Maks3w/FR3DLdapBundle)

有關更多信息,請參閱: https://github.com/ldaptools/ldaptools-bundle/blob/master/Resources/doc/Save-LDAP-Users-to-the-Database-After-Login.md

更新:

這就是我讓它工作的方式(沒有外部捆綁包):

  1. 安全性.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: username

        ldap_server:
            ldap:
                service: Symfony\Component\Ldap\Ldap
                base_dn: "dc=example,dc=com"
                search_dn: "cn=read-only-admin,dc=example,dc=com"
                search_password: "password"
                default_roles: ROLE_USER
                uid_key: uid

        chain_provider:
            chain:
                providers: [ 'app_user_provider', 'ldap_server' ]

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: lazy
            provider: chain_provider


            form_login:
                login_path: app_login
                check_path: app_login

            form_login_ldap:
                login_path: app_login
                check_path: app_login
                service: Symfony\Component\Ldap\Ldap
                dn_string: 'uid={username},dc=example,dc=com'

            logout:
                path: app_logout

  1. 事件監聽器
<?php


namespace App\EventListener;


use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;

class LoginEventListener
{
    /**
     * @var EntityManagerInterface
     */
    protected $em;

    /**
     * @var UserPasswordEncoderInterface
     */
    private $encoder;

    /**
     * LoginEventListener constructor.
     * @param EntityManagerInterface $em
     * @param UserPasswordEncoderInterface $encoder
     */
    public function __construct(EntityManagerInterface $em, UserPasswordEncoderInterface $encoder)
    {
        $this->em = $em;
        $this->encoder = $encoder;
    }

    /**
     * @param InteractiveLoginEvent $event
     */
    public function onLoginSuccess(InteractiveLoginEvent $event)
    {
        $request = $event->getRequest();
        $token = $event->getAuthenticationToken();
        $loggedUser = $token->getUser();

//     If the logged user is not an instance of User (not ldapUser), then it hasn't been saved to the database. So save it..
        if(!($loggedUser instanceof User)) {
            $user = new User();
            $user->setUsername($request->request->get('_username'));
            $user->setPassword($this->encoder->encodePassword($user, $request->request->get('_password')));
            $user->setRoles($loggedUser->getRoles());
            $this->em->persist($user);
            $this->em->flush();
        }

    }

  1. 服務.yaml
# ldap service
    Symfony\Component\Ldap\Ldap:
        arguments: [ '@Symfony\Component\Ldap\Adapter\ExtLdap\Adapter' ]
    Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
        arguments:
            - host: ldap.forumsys.com
              port: 389
              options:
                  protocol_version: 3
                  referrals: false

    app_bundle.event.login_listener:
        class: App\EventListener\LoginEventListener
        arguments: [ '@doctrine.orm.entity_manager', '@security.user_password_encoder.generic' ]
        tags:
            - { name: kernel.event_listener, event: security.interactive_login, method: onLoginSuccess }

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM