codecrafting-io/adoldap

使用 ADO 和 LDAP 在活动目录中无缝搜索的 PHP 方式

v0.2.0 2020-01-06 23:07 UTC

This package is auto-updated.

Last update: 2024-09-07 09:11:00 UTC


README

Latest Stable Version License Downloads

摘要

⚠️ 警告: ⚠️ 这个库仍在 alpha 阶段,因此正在测试中,新版本可能会破坏向后兼容性。

AdoLDAP 是一个小的 PHP 库,用于通过 ADO 和 LDAP 无缝地在活动目录中进行搜索和认证。简而言之,它提供了以下好处

  • ⭐ 无缝连接到活动目录,无需任何配置,甚至不需要服务器。
  • ⭐ 简单、可重用、有趣的语义语法。您可以使用预构建的搜索来查找用户、计算机和组,并以对象的形式处理它们,使用简单易读的 get/set 语法。
  • ⭐ 支持 LDAP 和 SQL 方言的流畅查询构建器。
  • ⭐ 工具来发现有关当前环境的信息,包括可用的域控制器、已连接的 DC、主 DC、域名等。
  • ⭐ 原生 PHP 数据类型处理。返回值没有奇怪的 VARIANT 对象,提供了一个优雅的迭代器接口来遍历对象。

如何工作

TOP

AdoLDAP 提供的主要功能是使用 LDAP 以执行线程的当前安全上下文无缝在 AD 上进行认证。这是通过 ADODB 活动目录接口 实现的功能,可以通过 COM 对象 在 PHP 语言(或任何 COM 兼容语言)中使用。通常这意味着如果当前用户已登录到域,并且此用户有权搜索 AD(这很可能会发生),则 ADODB 不需要特定的凭据来连接。

换句话说,现在您可以创建无需特定读写用户即可连接的 Web 应用程序,通过 LDAP 进行搜索。例如,您可以使用 Windows 认证 设置并利用无缝搜索有关当前认证用户的信息。您还可以在没有 Windows 认证的情况下在本地计算机上进行测试。

要求

TOP

  • Windows 环境。
  • PHP >= 7.1 64位
  • 启用 COM PHP 扩展
  • Composer

由于 COM 扩展是使用 ADODB 的方式,而 ADO 仅限于 Windows,因此 AdoLDAP 仅在 Windows 上运行。要安装 AdoLDAP,请使用以下命令

composer require codecrafting-io/adoldap

配置

TOP

域名信息

TOP

如前所述,您可以在不提供连接信息的情况下进行搜索。这是由于 ADODB 允许提供零信息连接的功能,包括凭据或连接的主机。如果您未提供关于主机或要用于连接的 baseDn 的信息,则库很可能连接到用户的 logonDomainController,即用户上次连接的域控制器主机。

要了解可用的域名信息,请使用以下代码

try {
    $ad = new AdoLDAP();
    dump($ad->info());
} catch(\Exception $e) {
    dump($e);
}

下表提供了每个返回属性的详细信息

连接配置

TOP

即使不需要提供单个配置来连接,也建议这样做,因为这可以确保搜索结果的一致性,尤其是在性能方面。强烈建议提供 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);
}

下表提供了每个配置的信息

搜索

TOP

有两种搜索方式,使用原始查询或通过 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);
}

查询构建器

TOP

查询构建器允许您使用 LDAP 和 SQL 语句轻松创建查询。主类 AdoLDAP 提供了一个 search 方法,这是一个新的 SearchFactory 实例。SearchFactory 可以设置一个新的 QueryBuilder 来构建搜索。您可以使用 SearchFactorynewQuery 方法来构建 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 由构建属性选择和条件子句的方法组成,用于从特定来源搜索,可以使用 LDAPDialectSQLDialect。您使用 select 方法构建 SELECT,使用 where 方法定义子句,可以选择使用 LDAPDialectSQLDialect。您还可以使用 from 方法更改和设置特定的 BASE_DN

$ad->search()
    ->select(['name'])
    ->from('DC=MYDOMAIN,DC=COM')
    ->whereEquals('objectCategory', 'user')
    ->orWhere('objectCategory', 'computer')
    ->get();

注意:使用 from 不需要定义基本 dn,因为配置中提供的值默认使用。

搜索方言

TOP

QueryBuilder 可以使用 LDAPSQL 语句进行搜索。您可以在配置中配置方言,如下面的示例所示

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 来实现这两个方言的自己的实现。

处理数据

TOP

创建搜索后,数据通过 ResultSetIterator 检索。使用 ADODB,结果数据通过具有类似游标或迭代器功能的 RecordSet 返回。因此,可以像在 foreach 中循环 array 一样简单地遍历 ResultSetIterator,它实现了 PHP 的本地类 SeekableIteratorCountable

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

通过 ResultSetIteratorcurrent 方法检索 RecordSet 的每个位置。数据通常以 Entry 的形式返回。一个 Entry 包含搜索返回的所有属性,并将它们作为 get/set 魔术属性公开,如上面的示例所示,或者您也可以使用 getAttributesetAttribute 来获取所需的值。如果属性不存在,则返回 null 值。

模型与列映射属性

TOP

大多数搜索实际上返回的是其中一个模型,可能是用户计算机。这个模型通过提供对“默认属性”的读取/设置方法来扩展条目,这增强了处理某些值的便利性。如果您发现很难理解其含义,或者根本不知道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'
];

因此,用户模型提供了使用映射名称的属性列表,但如果您更愿意使用原始名称方案,您仍然可以使用读取/设置魔术属性或getAttributesetAttribute方法。

$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-pictureIMG HTML标签,使用src作为图像的base64字符串表示。对于照片属性,您还可以通过使用相应的getPhotogetRawPhotosavePhoto方法获取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搜索单个条目,那么usercomputergroup方法还提供了一个参数,该参数已将属性翻译过来,实际上这些方法的默认行为就是翻译属性。

$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秒。所以只用于测试目的。

特殊属性

TOP

除了模型之外,一些属性作为对象处理,如DistinguishedNameOSAddressObjectClass

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处理与属性operatingSystemoperatingSystemVersion相关的操作系统值,这些值可以在计算机条目中找到。该类提取和拆分数据,并提供检索nameversionflavour的方法,还提供了轻松比较操作系统的方式。

$computer = $ad->search()->computer('MACHINE01');
echo $computer->getOS()->getName();
$computer->compareTo($ad->search()->computer('MACHEINE02')) //outputs -1, 0, 1;

分页数据

TOP

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

获取后回调

TOP

ResultSetIterator 还提供了一个 afterFetch 回调,允许您在每次调用 current 方法时转换条目。

$user = $ad->search()->user('jdoe')->afterFetch(function($user) {
                return [
                    'name' => $user->getName(),
                    'accountName' => $user->getAccount(),
                    'photo' => $user->getPhoto()
                ];
            })->getEntries();

您可以对 afterFetch 进行多次设置,这将按照提供的顺序多次转换数据。

即将推出

TOP

  • Active Record 模型。目前只提供搜索功能。
  • 事件支持。
  • 更健壮的 QueryBuilder
  • 全面完整的文档站点。
  • 缓存支持。

最后

感谢 Adldap2 为创建这个库提供了灵感。

@lucasmarotta 用 ❤️ 制作。