codecrafting-io / adoldap
使用 ADO 和 LDAP 在活动目录中无缝搜索的 PHP 方式
Requires
- php-64bit: >=7.1
- ext-com_dotnet: *
Requires (Dev)
- devster/ubench: ^2.1
- phpunit/phpunit: 6.*
- symfony/var-dumper: ^4
README
摘要
⚠️ 警告: ⚠️ 这个库仍在 alpha 阶段,因此正在测试中,新版本可能会破坏向后兼容性。
AdoLDAP 是一个小的 PHP 库,用于通过 ADO 和 LDAP 无缝地在活动目录中进行搜索和认证。简而言之,它提供了以下好处
- ⭐ 无缝连接到活动目录,无需任何配置,甚至不需要服务器。
- ⭐ 简单、可重用、有趣的语义语法。您可以使用预构建的搜索来查找用户、计算机和组,并以对象的形式处理它们,使用简单易读的 get/set 语法。
- ⭐ 支持 LDAP 和 SQL 方言的流畅查询构建器。
- ⭐ 工具来发现有关当前环境的信息,包括可用的域控制器、已连接的 DC、主 DC、域名等。
- ⭐ 原生 PHP 数据类型处理。返回值没有奇怪的
VARIANT对象,提供了一个优雅的迭代器接口来遍历对象。
如何工作
AdoLDAP 提供的主要功能是使用 LDAP 以执行线程的当前安全上下文无缝在 AD 上进行认证。这是通过 ADODB 活动目录接口 实现的功能,可以通过 COM 对象 在 PHP 语言(或任何 COM 兼容语言)中使用。通常这意味着如果当前用户已登录到域,并且此用户有权搜索 AD(这很可能会发生),则 ADODB 不需要特定的凭据来连接。
换句话说,现在您可以创建无需特定读写用户即可连接的 Web 应用程序,通过 LDAP 进行搜索。例如,您可以使用 Windows 认证 设置并利用无缝搜索有关当前认证用户的信息。您还可以在没有 Windows 认证的情况下在本地计算机上进行测试。
要求
- Windows 环境。
- PHP >= 7.1 64位
- 启用 COM PHP 扩展
- Composer
由于 COM 扩展是使用 ADODB 的方式,而 ADO 仅限于 Windows,因此 AdoLDAP 仅在 Windows 上运行。要安装 AdoLDAP,请使用以下命令
composer require codecrafting-io/adoldap
配置
域名信息
如前所述,您可以在不提供连接信息的情况下进行搜索。这是由于 ADODB 允许提供零信息连接的功能,包括凭据或连接的主机。如果您未提供关于主机或要用于连接的 baseDn 的信息,则库很可能连接到用户的 logonDomainController,即用户上次连接的域控制器主机。
要了解可用的域名信息,请使用以下代码
try { $ad = new AdoLDAP(); dump($ad->info()); } catch(\Exception $e) { dump($e); }
下表提供了每个返回属性的详细信息
连接配置
即使不需要提供单个配置来连接,也建议这样做,因为这可以确保搜索结果的一致性,尤其是在性能方面。强烈建议提供 BASE_DN(您可以使用 defaultNamingContext),对于服务器来说,使用 primaryDomainControllers 更为可取,以便更快地连接。因此,在检查上述代码返回的值后,按照以下代码进行连接
try { //Minimal recommended configuration $config = [ 'host' => 'server01.mydomain.com', //use a primaryDomainController 'baseDn' => 'DC=MYDOMAIN,DC=COM', ]; $ad = new AdoLDAP($config); //auto connected } catch(\AdoLDAPException $e) { dump($e); }
下表提供了每个配置的信息
搜索
有两种搜索方式,使用原始查询或通过 QueryBuilder 构建。
使用原始查询进行搜索
try { $config = [ 'host' => 'server01.mydomain.com', //use a primaryDomainController 'baseDn' => 'DC=MYDOMAIN,DC=COM', ]; $ad = new AdoLDAP($config); $ad->search()->query("<LDAP://server01.mydomain.com/DC=MYDOMAIN,DC=COM>;(&(objectCategory=user)(sAMAccountName=jdoe));sAMAccountName,name"); } catch(\AdoLDAPException $e) { dump($e); }
使用 QueryBuilder 进行搜索
try { $config = [ 'host' => 'server01.mydomain.com', //use a primaryDomainController 'baseDn' => 'DC=MYDOMAIN,DC=COM', ]; $ad = new AdoLDAP($config); $ad->search()->whereEquals('objectCategory', 'user')->findBy('sAMAccountName', 'jdoe'); } catch(\AdoLDAPException $e) { dump($e); }
查询构建器
查询构建器允许您使用 LDAP 和 SQL 语句轻松创建查询。主类 AdoLDAP 提供了一个 search 方法,这是一个新的 SearchFactory 实例。SearchFactory 可以设置一个新的 QueryBuilder 来构建搜索。您可以使用 SearchFactory 的 newQuery 方法来构建 QueryBuilder,但由于 SearchFactory 上存在的魔术方法,您也可以使用任何可用的 QueryBuilder 方法,如下面的示例所示
$ad->search()->newQuery()->whereEquals('objectCategory', 'user')->findBy('sAMAccountName', 'jdoe'); //OR $ad->search()->whereEquals('objectCategory', 'user')->findBy('sAMAccountName', 'jdoe');
SearchFactory 还提供了预构建的或范围限定搜索,以方便搜索用户、计算机和组。
$ad->search()->users(); //EQUALS $ad->search()->whereEquals('objectCategory', 'user');
$ad->search()->user('jdoe', ['name']); //EQUALS $ad->search()->whereEquals('objectCategory', 'user')->firstBy('sAMAccountName', 'jdoe', ['name']);
总结来说,QueryBuilder 由构建属性选择和条件子句的方法组成,用于从特定来源搜索,可以使用 LDAPDialect 或 SQLDialect。您使用 select 方法构建 SELECT,使用 where 方法定义子句,可以选择使用 LDAPDialect 或 SQLDialect。您还可以使用 from 方法更改和设置特定的 BASE_DN。
$ad->search() ->select(['name']) ->from('DC=MYDOMAIN,DC=COM') ->whereEquals('objectCategory', 'user') ->orWhere('objectCategory', 'computer') ->get();
注意:使用 from 不需要定义基本 dn,因为配置中提供的值默认使用。
搜索方言
QueryBuilder 可以使用 LDAP 和 SQL 语句进行搜索。您可以在配置中配置方言,如下面的示例所示
try { $config = [ 'host' => 'server01.mydomain.com', 'baseDn' => 'DC=MYDOMAIN,DC=COM', 'dialect' => SQLDialect::class ]; $ad = new AdoLDAP($config); } catch(\AdoLDAPException $e) { dump($e); }
默认方言是 LDAPDialect。您可以通过实现 DialectInterface 来实现这两个方言的自己的实现。
处理数据
创建搜索后,数据通过 ResultSetIterator 检索。使用 ADODB,结果数据通过具有类似游标或迭代器功能的 RecordSet 返回。因此,可以像在 foreach 中循环 array 一样简单地遍历 ResultSetIterator,它实现了 PHP 的本地类 SeekableIterator 和 Countable。
$users = $ad->search()->users() ->select(['sAMAccountName', 'name', 'thumbnailPhoto', 'mail']) ->whereMemberOf('CN=AWESOME GROUP,DC=MYDOMAIN,DC=COM')->get(); foreach($users as $user) { echo $user->sAMAccountName . '<br>'; echo $user->name . '<br>'; echo $user->thumbnailPhoto . '<br>'; echo $user->mail . '<br>'; }
通过 ResultSetIterator 的 current 方法检索 RecordSet 的每个位置。数据通常以 Entry 的形式返回。一个 Entry 包含搜索返回的所有属性,并将它们作为 get/set 魔术属性公开,如上面的示例所示,或者您也可以使用 getAttribute 和 setAttribute 来获取所需的值。如果属性不存在,则返回 null 值。
模型与列映射属性
大多数搜索实际上返回的是其中一个模型,可能是用户、计算机或组。这个模型通过提供对“默认属性”的读取/设置方法来扩展条目,这增强了处理某些值的便利性。如果您发现很难理解其含义,或者根本不知道LDAP中对象的可用主要属性,那么模型提供了一个COLUMN_MAP,它将对应AD对象的最重要的属性映射到更“易于阅读”的名称。例如,看看用户的COLUMN_MAP。
const COLUMN_MAP = [ 'objectclass' => 'objectclass', 'dn' => 'distinguishedname', 'account' => 'samaccountname', 'firtname' => 'givenname', 'name' => 'name', 'workstations' => 'userworkstations', 'mail' => 'mail', 'jobtitle' => 'description', 'jobrole' => 'title', 'address' => [ 'street', 'postalcode', 'st', 'l', 'co' ], 'mailboxes' => 'msexchdelegatelistbl', 'mobile' => 'mobile', 'phone' => 'telephoneNumber', 'department' => 'department', 'departmentcode' => 'extensionAttribute1', 'memberOf' => 'memberOf', 'company' => 'company', 'photo' => 'thumbnailphoto', 'passwordlastset' => 'pwdlastset', 'passworderrorcount' => 'badpwdcount', 'passworderrortime' => 'badpasswordtime', 'lastlogin' => 'lastlogontimestamp', 'lockouttime' => 'lockouttime', 'createdat' => 'whencreated', 'objectguid' => 'objectguid', 'objectsid' => 'objectsid' ];
因此,用户模型提供了使用映射名称的属性列表,但如果您更愿意使用原始名称方案,您仍然可以使用读取/设置魔术属性或getAttribute和setAttribute方法。
$users = $ad->search()->users() ->select(['sAMAccountName', 'name', 'thumbnailPhoto', 'mail']) ->whereMemberOf('CN=AWESOME GROUP,DC=MYDOMAIN,DC=COM')->get(); foreach($users as $user) { echo $user->getAccount(); //EQUALS echo $user->sAMAccountName; }
可用的映射属性物理存在于模型类中,以方便IDE的自动完成功能。模型还可以提供特殊处理,以方便使用某些属性,如用户照片。
$users = $ad->search()->users() ->select(['sAMAccountName', 'name', 'thumbnailPhoto']) ->whereMemberOf('CN=AWESOME GROUP,DC=MYDOMAIN,DC=COM')->get(); foreach($users as $user) { echo $user->getAccount() . '<br>'; echo $user->getName() . '<br>'; echo $user->getHtmlPhoto(['class' => 'profile-picture']) . '<br>'; }
getHtmlPhoto返回一个包含类profile-picture的IMG HTML标签,使用src作为图像的base64字符串表示。对于照片属性,您还可以通过使用相应的getPhoto、getRawPhoto和savePhoto方法获取base64、原始二进制字符串,甚至将照片保存到文件。
您还可以在QueryBuilder选择中使用映射属性。
$attributes = SearchFactory::translateAttributes(User::COLUMN_MAP, ['accountName', 'name', 'photo', 'mailboxes']); $ad->search()->users() ->select($attributes) ->whereMemberOf('CN=AWESOME GROUP,DC=MYDOMAIN,DC=COM')->get(); }
如果您使用SearchFactory搜索单个条目,那么user、computer和group方法还提供了一个参数,该参数已将属性翻译过来,实际上这些方法的默认行为就是翻译属性。
$ad->search()->user('jdoe', ['accountName', 'name', 'photo', 'mailboxes']); //EQUALS $ad->search()->user('jdoe', ['sAMAccountName', 'name', 'thumbnailPhoto', 'msExchDelegateListBl'], false);
如果您没有向这些方法提供属性,则将使用整个COLUMN_MAP。
$ad->search()->user('jdoe'); //EQUALS $ad->search()->user('jdoe', User::getDefaultAttributes(), false);
⚠️ 重要:⚠️可以通过提供值
['*']来选择所有属性,但这被强烈反对,不仅因为可能有很多可能不会使用的属性,而且主要是因为性能原因。当您使用查询并使用通配符*时,ADODB返回ADSPATH,即对象的完整区分名称,迫使库通过使用COM直接绑定到对象来解析它们,这甚至对单个对象来说也非常慢。通常搜索一个条目需要大约200-400毫秒,但绑定到对象可能需要4秒。所以只用于测试目的。
特殊属性
除了模型之外,一些属性作为对象处理,如DistinguishedName、OS、Address和ObjectClass。
DistinguidedName
如名称所示,DistinguishedName类处理区分名称,提供了从名称中提取组件和部分的简单方法。
$dn = new DistinguishedName('CN=AWESOME GROUP,DC=MYDOMAIN,DC=COM'); echo $dn->getName() . '<br>'; //Container Name - AWESOME GROUP echo $dn->getPath() //Whole Path - CN=AWESOME GROUP,DC=MYDOMAIN,DC=COM
ObjectClass
ObjectClass是对objectClass属性数组的简单对象包装。该类还提供了getMostRelevant方法,这是最后一个也是最重要的ObjectClass名称。每个模型都已经定义了一个objectClass,可以通过使用静态方法objectClass来获取。
echo User::objectClass()->getMostRelevant() //outputs user
Address
Address是一个简单的POJO对象,用于存储在User条目上出现的所有地址相关属性。您会注意到COLUMN_MAP将地址映射为4个其他属性,这些属性被展开以在QueryBuilder上执行选择。地址还有一个更清晰的语言和命名方案。
$user = $ad->search()->user('jdoe', ['accountName', 'address']); //EQUALS TO: $ad->search()->user('jdoe', ['sAMAcountName', 'street', 'postalCode', 'st', 'l', 'co']); echo $user->getAddress()->getCountry();
OS
OS处理与属性operatingSystem和operatingSystemVersion相关的操作系统值,这些值可以在计算机条目中找到。该类提取和拆分数据,并提供检索name、version、flavour的方法,还提供了轻松比较操作系统的方式。
$computer = $ad->search()->computer('MACHINE01'); echo $computer->getOS()->getName(); $computer->compareTo($ad->search()->computer('MACHEINE02')) //outputs -1, 0, 1;
分页数据
ADODB 通过使用由 ResultSetIterator 管理的 RecordSet 本地分页结果。您可以通过设置 pageSize 配置项的值来提供特定的 LDAP 分页大小。默认值是 1000。不过,ResultSetIterator 还提供了通过 getEntries 方法实现的单独的分页功能,这允许您定义合适的限制/偏移量。
//Get 10 users offseting 1 page. Returns a array of User objects $users = $ad->search()->users() ->select(User::getDefaultAttributes()) ->whereMemberOf('CN=AWESOME GROUP,DC=MYDOMAIN,DC=COM') ->get()->getEntries(10, 1);
获取后回调
ResultSetIterator 还提供了一个 afterFetch 回调,允许您在每次调用 current 方法时转换条目。
$user = $ad->search()->user('jdoe')->afterFetch(function($user) { return [ 'name' => $user->getName(), 'accountName' => $user->getAccount(), 'photo' => $user->getPhoto() ]; })->getEntries();
您可以对 afterFetch 进行多次设置,这将按照提供的顺序多次转换数据。
即将推出
- Active Record
模型。目前只提供搜索功能。 - 事件支持。
- 更健壮的
QueryBuilder。 - 全面完整的文档站点。
- 缓存支持。
最后
感谢 Adldap2 为创建这个库提供了灵感。
由 @lucasmarotta 用 ❤️ 制作。