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
这就是我的工作方式(没有外部捆绑包):
- 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
- 事件监听器
<?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();
}
}
- 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 }
相关文章