Symfony 5.1:使用实体用户提供者的 LDAP 身份验证 [英] Symfony 5.1: LDAP Authentication with Entity User Provider

查看:22
本文介绍了Symfony 5.1:使用实体用户提供者的 LDAP 身份验证的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用 Symfony 5.1 并尝试实现 LDAP 身份验证,而用户属性(用户名、角色等)存储在 MySQL 数据库中.因此,我为 Doctrine 添加了一个用户实体,并配置了文档对应的 services.yml 和 security.yml.

I'm using Symfony 5.1 and trying to implement a LDAP Authentication, while the User Properties (Username, Roles, etc.) are stored in a MySQL DB. Thus I added a User Entity for Doctrine and configurated the services.yml and security.yml corresponding to the Documentation.

我还使用 Maker Bundle 生成了一个 LoginFormAuthenticator,它似乎使用了 Guard Authenticator 模块.

I also used the Maker Bundle to generate a LoginFormAuthenticator which seems to use the Guard Authenticator Module.

当我尝试登录时,它看起来好像没有做任何与 LDAP 相关的事情.我还用 tcpdump 监听了 TCP 包,没有看到任何到 LDAP 服务器的流量.

When I'm trying to login it simply looks like it is not doing anything LDAP related. I also listened the TCP packages with tcpdump and didn't see any traffic to the LDAP server.

这是我的代码:

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}'

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

    SymfonyComponentLdapLdap:
        arguments: ['@SymfonyComponentLdapAdapterExtLdapAdapter']
    SymfonyComponentLdapAdapterExtLdapAdapter:
        arguments:
            -   host: <ldap-IP>
                port: 389
                options:
                    protocol_version: 3
                    referrals: false

security.yml:

security:
    encoders:
        AppEntityUser:
            algorithm: auto

        app_user_provider:
            entity:
                class: AppEntityUser
                property: email

        my_ldap:
            ldap:
                service: SymfonyComponentLdapLdap
                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: SymfonyComponentLdapLdap
                dn_string: 'uid={username},OU=Test,DC=domain,DC=domain'

            guard:
                authenticators:
                    - AppSecurityLoginFormAuthenticator
            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 函数中.我发现 LdapBindAuthenticationProvider 类的目的似乎正是针对 LDAP 进行此类用户凭据检查,但我完全不确定该怎么做:

The LoginFormAuthenticator, I guess the issue lies here within the checkCredentials function. I found the LdapBindAuthenticationProvider class which's purpose seems to be exactly such user credential checking agains LDAP, but I'm totally unsure how I have to do it:

<?php

namespace AppSecurity;

use PsrLogLoggerInterface;
use AppEntityUser;
use DoctrineORMEntityManagerInterface;
use SymfonyComponentHttpFoundationRedirectResponse;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentRoutingGeneratorUrlGeneratorInterface;
use SymfonyComponentSecurityCoreAuthenticationTokenTokenInterface;
use SymfonyComponentSecurityCoreEncoderUserPasswordEncoderInterface;
use SymfonyComponentSecurityCoreExceptionCustomUserMessageAuthenticationException;
use SymfonyComponentSecurityCoreExceptionInvalidCsrfTokenException;
use SymfonyComponentSecurityCoreSecurity;
use SymfonyComponentSecurityCoreUserUserInterface;
use SymfonyComponentSecurityCoreUserUserProviderInterface;
use SymfonyComponentSecurityCsrfCsrfToken;
use SymfonyComponentSecurityCsrfCsrfTokenManagerInterface;
use SymfonyComponentSecurityGuardAuthenticatorAbstractFormLoginAuthenticator;
use SymfonyComponentSecurityGuardPasswordAuthenticatedInterface;
use SymfonyComponentSecurityHttpUtilTargetPathTrait;
use SymfonyComponentLdapLdap;
use SymfonyComponentSecurityCoreAuthenticationProviderLdapBindAuthenticationProvider;

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

很遗憾,我没有找到任何示例代码.

Unfortunately I didn't find any example code for this.

感谢 T. van den Berg 的回答,我终于设法让身份验证部分正常工作.我从 security.yml 中删除了 LoginFormAuthenticator Guard,并稍微调整了 form_login_ldap.

Thanks to the answer of T. van den Berg I finally managed to get the authentication part working. I removed the LoginFormAuthenticator Guard from the security.yml and tweaked the form_login_ldap a little bit.

security:
    encoders:
        AppEntityUser:
            algorithm: auto

    providers:
        app_user_provider:
            entity:
                class: AppEntityUser
                property: email

        my_ldap:
            ldap:
                service: SymfonyComponentLdapLdap
                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: SymfonyComponentLdapLdap
                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 }

它现在使用 LDAPUserProvider 使用 LDAP 服务用户(绑定 DN)通过其登录名(sAMAccountName)获取用户 LDAP 对象,然后在第二个请求中使用此 LDAP 对象的专有名称(DN)来使用提供的密码对 LDAP 服务器进行另一次身份验证.到目前为止还不错.

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 against the LDAP server with the provided password. That's fine so far.

唯一缺少的是数据库存储的用户实体.我的想法如下:

The only thing missing is the database stored User entity. My Idea was as follows:

  • 登录表单已提交
  • 使用提供的用户名在数据库中搜索用户实体
  • 如果未找到用户实体,请使用 LDAPUserProvider 向 LDAP 询问用户名
  • 如果用户存在于 LDAP 中,则在数据库中创建一个用户实体
  • 使用提供的密码针对 LDAP 对用户进行身份验证

密码未保存在数据库中,但其他应用程序特定信息在 LDAP 中不可用(例如上次活动).

The password is not saved in the database, but other application specific information not available in LDAP (e.g. last activity).

有人知道如何实现吗?

推荐答案

如果您想将 LDAP 用户保存到登录后的本地数据库.

You can use this bundle ldaptools/ldaptools-bundle (or Maks3w/FR3DLdapBundle) if you want to save your LDAP user to a local database after they login.

有关更多信息,请参阅:https://github.com/ldaptools/ldaptools-bundle/blob/master/Resources/doc/Save-LDAP-Users-to-the-Database-After-Login.md

for more information see: https://github.com/ldaptools/ldaptools-bundle/blob/master/Resources/doc/Save-LDAP-Users-to-the-Database-After-Login.md

这就是我的工作方式(没有外部捆绑包):

  1. Security.yaml

security:
    encoders:
        AppEntityUser:
            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: AppEntityUser
                property: username

        ldap_server:
            ldap:
                service: SymfonyComponentLdapLdap
                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: SymfonyComponentLdapLdap
                dn_string: 'uid={username},dc=example,dc=com'

            logout:
                path: app_logout

  1. 事件监听器

<?php


namespace AppEventListener;


use AppEntityUser;
use DoctrineORMEntityManagerInterface;
use SymfonyComponentSecurityCoreEncoderUserPasswordEncoderInterface;
use SymfonyComponentSecurityHttpEventInteractiveLoginEvent;

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. services.yaml

# ldap service
    SymfonyComponentLdapLdap:
        arguments: [ '@SymfonyComponentLdapAdapterExtLdapAdapter' ]
    SymfonyComponentLdapAdapterExtLdapAdapter:
        arguments:
            - host: ldap.forumsys.com
              port: 389
              options:
                  protocol_version: 3
                  referrals: false

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

这篇关于Symfony 5.1:使用实体用户提供者的 LDAP 身份验证的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆