PHP项目Symfony ldap与目录

wen PHP项目 1

本文目录导读:

PHP项目Symfony ldap与目录

  1. 基础LDAP配置
  2. LDAP认证服务
  3. 自定义LDAP用户提供器
  4. 自定义LDAP认证器
  5. 安全配置
  6. 高级功能实现
  7. 测试LDAP连接
  8. 最佳实践

我来帮你详细介绍Symfony框架中LDAP与目录服务集成的实现方案。

基础LDAP配置

安装LDAP组件

composer require symfony/ldap

配置LDAP连接

# config/packages/ldap.yaml
ldap:
    # 默认连接
    default:
        host: 'ldap.example.com'
        port: 389
        encryption: tls  # tls, ssl, none
        options:
            protocol_version: 3
            referrals: false
    # 备用连接
    backup:
        host: 'backup-ldap.example.com'
        port: 636
        encryption: ssl
        options:
            protocol_version: 3
            network_timeout: 10

LDAP认证服务

创建LDAP服务类

<?php
namespace App\Service;
use Symfony\Component\Ldap\Ldap;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Ldap\Exception\LdapException;
class LdapService
{
    private Ldap $ldap;
    private string $baseDn;
    public function __construct(Ldap $ldap, string $baseDn = 'dc=example,dc=com')
    {
        $this->ldap = $ldap;
        $this->baseDn = $baseDn;
    }
    /**
     * 用户认证
     */
    public function authenticate(string $username, string $password): ?Entry
    {
        try {
            // 绑定管理员账号(用于搜索)
            $this->ldap->bind('cn=admin,dc=example,dc=com', 'admin_password');
            // 搜索用户
            $query = $this->ldap->query(
                $this->baseDn,
                sprintf('(uid=%s)', $username)
            );
            $entries = $query->execute();
            if ($entries->count() === 0) {
                return null;
            }
            $userEntry = $entries[0];
            $userDn = $userEntry->getDn();
            // 验证用户密码
            $this->ldap->bind($userDn, $password);
            return $userEntry;
        } catch (LdapException $e) {
            throw new \RuntimeException('LDAP认证失败: ' . $e->getMessage());
        }
    }
    /**
     * 查询用户信息
     */
    public function findUser(string $uid): ?Entry
    {
        try {
            $this->ldap->bind('cn=admin,dc=example,dc=com', 'admin_password');
            $query = $this->ldap->query(
                $this->baseDn,
                sprintf('(|(uid=%s)(mail=%s))', $uid, $uid)
            );
            $results = $query->execute();
            return $results->count() > 0 ? $results[0] : null;
        } catch (LdapException $e) {
            return null;
        }
    }
    /**
     * 获取用户组信息
     */
    public function getUserGroups(string $userDn): array
    {
        try {
            $this->ldap->bind('cn=admin,dc=example,dc=com', 'admin_password');
            $query = $this->ldap->query(
                $this->baseDn,
                sprintf('(member=%s)', $userDn)
            );
            $results = $query->execute();
            $groups = [];
            foreach ($results as $entry) {
                $groups[] = $entry->getAttribute('cn')[0];
            }
            return $groups;
        } catch (LdapException $e) {
            return [];
        }
    }
}

自定义LDAP用户提供器

创建用户实体

<?php
namespace App\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
class LdapUser implements UserInterface
{
    private string $username;
    private array $roles;
    private string $email;
    private string $displayName;
    public function __construct(string $username, array $roles = ['ROLE_USER'])
    {
        $this->username = $username;
        $this->roles = $roles;
    }
    public function getRoles(): array
    {
        return $this->roles;
    }
    public function getPassword(): ?string
    {
        return null; // LDAP管理密码
    }
    public function getSalt(): ?string
    {
        return null;
    }
    public function getUsername(): string
    {
        return $this->username;
    }
    public function getUserIdentifier(): string
    {
        return $this->username;
    }
    public function eraseCredentials(): void
    {
        // 如果需要清除敏感数据
    }
    // Getter/Setter
    public function getEmail(): string
    {
        return $this->email;
    }
    public function setEmail(string $email): void
    {
        $this->email = $email;
    }
    public function getDisplayName(): string
    {
        return $this->displayName;
    }
    public function setDisplayName(string $displayName): void
    {
        $this->displayName = $displayName;
    }
}

创建LDAP用户提供器

<?php
namespace App\Security;
use App\Entity\LdapUser;
use App\Service\LdapService;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class LdapUserProvider implements UserProviderInterface
{
    private LdapService $ldapService;
    public function __construct(LdapService $ldapService)
    {
        $this->ldapService = $ldapService;
    }
    public function loadUserByIdentifier(string $identifier): UserInterface
    {
        $entry = $this->ldapService->findUser($identifier);
        if (!$entry) {
            throw new UserNotFoundException(sprintf('User "%s" not found', $identifier));
        }
        return $this->createUserFromEntry($entry);
    }
    public function loadUserByUsername(string $username): UserInterface
    {
        return $this->loadUserByIdentifier($username);
    }
    public function refreshUser(UserInterface $user): UserInterface
    {
        if (!$user instanceof LdapUser) {
            throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
        }
        return $this->loadUserByIdentifier($user->getUserIdentifier());
    }
    public function supportsClass(string $class): bool
    {
        return LdapUser::class === $class;
    }
    private function createUserFromEntry(Entry $entry): LdapUser
    {
        $username = $entry->getAttribute('uid')[0];
        $email = $entry->getAttribute('mail')[0] ?? '';
        $displayName = $entry->getAttribute('displayName')[0] ?? $username;
        // 基于LDAP组分配角色
        $roles = $this->determineRoles($entry);
        $user = new LdapUser($username, $roles);
        $user->setEmail($email);
        $user->setDisplayName($displayName);
        return $user;
    }
    private function determineRoles(Entry $entry): array
    {
        $roles = ['ROLE_USER'];
        $groups = $this->ldapService->getUserGroups($entry->getDn());
        // 映射LDAP组到角色
        $roleMappings = [
            'admins' => 'ROLE_ADMIN',
            'managers' => 'ROLE_MANAGER',
        ];
        foreach ($groups as $group) {
            if (isset($roleMappings[$group])) {
                $roles[] = $roleMappings[$group];
            }
        }
        return $roles;
    }
}

自定义LDAP认证器

创建认证器

<?php
namespace App\Security;
use App\Service\LdapService;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
class LdapAuthenticator extends AbstractAuthenticator
{
    private LdapService $ldapService;
    private LdapUserProvider $userProvider;
    public function __construct(LdapService $ldapService, LdapUserProvider $userProvider)
    {
        $this->ldapService = $ldapService;
        $this->userProvider = $userProvider;
    }
    public function supports(Request $request): ?bool
    {
        return $request->attributes->get('_route') === 'app_login'
            && $request->isMethod('POST');
    }
    public function authenticate(Request $request): Passport
    {
        $username = $request->request->get('_username', '');
        $password = $request->request->get('_password', '');
        // LDAP认证
        $userEntry = $this->ldapService->authenticate($username, $password);
        if (!$userEntry) {
            throw new AuthenticationException('Invalid credentials.');
        }
        return new SelfValidatingPassport(
            new UserBadge($username, function($username) {
                return $this->userProvider->loadUserByIdentifier($username);
            })
        );
    }
    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    {
        return new RedirectResponse('/dashboard');
    }
    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    {
        $request->getSession()->getFlashBag()->add('error', 'Login failed: ' . $exception->getMessage());
        return new RedirectResponse('/login');
    }
}

安全配置

# config/packages/security.yaml
security:
    providers:
        ldap_users:
            id: App\Security\LdapUserProvider
    firewalls:
        main:
            lazy: true
            provider: ldap_users
            custom_authenticators:
                - App\Security\LdapAuthenticator
            logout:
                path: app_logout
                target: app_login
            # 登录表单配置
            form_login:
                login_path: app_login
                check_path: app_login
            # 记住登录功能
            remember_me:
                secret: '%kernel.secret%'
                lifetime: 604800 # 7天
                path: /
    access_control:
        - { path: ^/login, roles: PUBLIC_ACCESS }
        - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/dashboard, roles: ROLE_USER }

高级功能实现

LDAP用户同步

<?php
namespace App\Service;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\User;
use Symfony\Component\Ldap\Ldap;
class LdapSyncService
{
    private Ldap $ldap;
    private EntityManagerInterface $entityManager;
    private string $baseDn;
    public function __construct(Ldap $ldap, EntityManagerInterface $entityManager, string $baseDn = 'dc=example,dc=com')
    {
        $this->ldap = $ldap;
        $this->entityManager = $entityManager;
        $this->baseDn = $baseDn;
    }
    /**
     * 同步所有用户
     */
    public function syncAllUsers(): SyncResult
    {
        $result = new SyncResult();
        try {
            $this->ldap->bind('cn=admin,dc=example,dc=com', 'admin_password');
            $query = $this->ldap->query(
                $this->baseDn,
                '(objectClass=inetOrgPerson)',
                ['attributes' => ['uid', 'mail', 'cn', 'sn', 'department']]
            );
            $entries = $query->execute();
            foreach ($entries as $entry) {
                $uid = $entry->getAttribute('uid')[0];
                // 检查用户是否存在
                $existingUser = $this->entityManager
                    ->getRepository(User::class)
                    ->findOneBy(['username' => $uid]);
                if ($existingUser) {
                    $this->updateUser($existingUser, $entry);
                    $result->updated++;
                } else {
                    $this->createUser($entry);
                    $result->created++;
                }
            }
            $this->entityManager->flush();
        } catch (\Exception $e) {
            $result->errors[] = $e->getMessage();
        }
        return $result;
    }
    private function createUser(Entry $entry): void
    {
        $user = new User();
        $user->setUsername($entry->getAttribute('uid')[0]);
        $user->setEmail($entry->getAttribute('mail')[0] ?? '');
        $user->setDisplayName($entry->getAttribute('cn')[0] ?? '');
        $user->setDepartment($entry->getAttribute('department')[0] ?? '');
        $user->setEnabled(true);
        $this->entityManager->persist($user);
    }
    private function updateUser(User $user, Entry $entry): void
    {
        $user->setEmail($entry->getAttribute('mail')[0] ?? $user->getEmail());
        $user->setDisplayName($entry->getAttribute('cn')[0] ?? $user->getDisplayName());
        $user->setDepartment($entry->getAttribute('department')[0] ?? $user->getDepartment());
    }
}
class SyncResult
{
    public int $created = 0;
    public int $updated = 0;
    public int $deleted = 0;
    public array $errors = [];
}

LDAP目录浏览器

<?php
namespace App\Controller;
use App\Service\LdapService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class LdapBrowserController extends AbstractController
{
    private LdapService $ldapService;
    public function __construct(LdapService $ldapService)
    {
        $this->ldapService = $ldapService;
    }
    #[Route('/admin/ldap/browse', name: 'ldap_browse')]
    public function browse(Request $request): Response
    {
        $baseDn = $request->query->get('dn', 'dc=example,dc=com');
        $filter = $request->query->get('filter', '(objectClass=*)');
        try {
            $entries = $this->ldapService->search($baseDn, $filter);
            return $this->render('admin/ldap/browse.html.twig', [
                'entries' => $entries,
                'baseDn' => $baseDn,
                'filter' => $filter,
            ]);
        } catch (\Exception $e) {
            $this->addFlash('error', 'LDAP browse failed: ' . $e->getMessage());
            return $this->redirectToRoute('admin_dashboard');
        }
    }
    #[Route('/admin/ldap/entry/{dn}', name: 'ldap_entry')]
    public function viewEntry(string $dn): Response
    {
        try {
            $entry = $this->ldapService->getEntry($dn);
            return $this->render('admin/ldap/entry.html.twig', [
                'entry' => $entry,
            ]);
        } catch (\Exception $e) {
            $this->addFlash('error', 'Failed to load LDAP entry: ' . $e->getMessage());
            return $this->redirectToRoute('ldap_browse');
        }
    }
}

测试LDAP连接

<?php
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Ldap\Ldap;
class LdapTestCommand extends Command
{
    protected static $defaultName = 'app:ldap:test';
    private Ldap $ldap;
    public function __construct(Ldap $ldap)
    {
        parent::__construct();
        $this->ldap = $ldap;
    }
    protected function configure(): void
    {
        $this
            ->setDescription('Test LDAP connection and configuration')
            ->addOption('username', 'u', InputOption::VALUE_REQUIRED, 'Username to test')
            ->addOption('password', 'p', InputOption::VALUE_REQUIRED, 'Password to test');
    }
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $io->title('LDAP Connection Test');
        try {
            // 测试基本连接
            $io->section('Testing connection...');
            $this->ldap->bind();
            $io->success('Connection successful');
            // 如果提供了用户名和密码
            if ($username = $input->getOption('username')) {
                $password = $input->getOption('password');
                $io->section('Testing authentication...');
                try {
                    $this->ldap->bind(
                        sprintf('uid=%s,ou=users,dc=example,dc=com', $username),
                        $password
                    );
                    $io->success("Authentication successful for user: $username");
                } catch (\Exception $e) {
                    $io->error("Authentication failed: " . $e->getMessage());
                }
            }
            // 显示服务器信息
            $io->section('Server Information');
            $io->table(
                ['Property', 'Value'],
                [
                    ['Host', getenv('LDAP_HOST') ?: 'localhost'],
                    ['Port', getenv('LDAP_PORT') ?: '389'],
                    ['Base DN', 'dc=example,dc=com'],
                    ['Protocol', 'LDAPv3'],
                ]
            );
        } catch (\Exception $e) {
            $io->error('Connection failed: ' . $e->getMessage());
            return Command::FAILURE;
        }
        return Command::SUCCESS;
    }
}

最佳实践

错误处理

// 创建LDAP异常监听器
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\Ldap\Exception\LdapException;
class LdapExceptionSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            ExceptionEvent::class => 'onKernelException',
        ];
    }
    public function onKernelException(ExceptionEvent $event): void
    {
        $exception = $event->getThrowable();
        if ($exception instanceof LdapException) {
            // 记录日志
            // 返回用户友好的错误信息
        }
    }
}

安全建议

# config/packages/ldap.yaml
ldap:
    default:
        host: '%env(LDAP_HOST)%'
        port: '%env(int:LDAP_PORT)%'
        encryption: tls
        options:
            protocol_version: 3
            # 安全选项
            network_timeout: 5
            ssl:
                verify_peer: true
                verify_peer_name: true
                allow_self_signed: false
        # 连接池配置
        connection_pool:
            max_connections: 10
            min_connections: 2
            idle_timeout: 300

这个完整的LDAP集成方案包含了从基础配置到高级功能的所有内容,可以根据你的具体需求进行调整和扩展。

抱歉,评论功能暂时关闭!