本文目录导读:

我来帮你详细介绍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集成方案包含了从基础配置到高级功能的所有内容,可以根据你的具体需求进行调整和扩展。