无需 System.Directory Services 的高效 Active Directory 查询





4.00/5 (7投票s)
使用 Novell 的 LDAP 库搜索目录服务。
引言
目录服务是企业运行的关键组件,随着大多数组织以某种形式采用目录服务,无论是管理用户帐户、配置电子邮件或存储空间,还是仅报告其控制下的资产,几乎所有这些任务都不可避免地需要自动化。虽然 .NET 框架提供了现有的库来与目录服务交互,但一直有人抱怨这些库的速度慢,并且与 OpenLDAP 和 Novell Directory Services (NDS) 等竞争平台的可互操作性存在疑问。Novell 最近发布了一个开源库,用于与目录服务交互,该库在所有符合 X.500 标准的目录中运行方式相同,本文将重点介绍该库的用法,特别是其搜索用法。本文可视为 如何:通过 C# 实现 Active Directory 中的一切 的扩展。
背景
在我最近参加的一次会议上,我遇到了一位发布了商业 Active Directory 库(AD-Advantage)的人,该库在 A/D 中似乎能做很多有趣的事情。我从这次交流中得知,Novell 发布了一个完全用托管代码编写的开源 LDAP 库。Microsoft 的 System.DirectoryServices
不是托管代码,因为它只是一个包装现有 ADSI COM 接口的层。我的职业生涯很大一部分都专注于 Active Directory,所以这个消息对我来说非常令人兴奋。一个适用于任何符合 X.500 标准的目录的开源 LDAP 接口意味着真正的跨平台代码可移植性。本文将演示如何实现 Novell.Directory.Ldap 库中的一些功能。
X.500 标准
X.500 标准最初是为了支持处理电子邮件和条目查找的 X.400 标准而开发的。X.500 定义了几个协议,但这些细节超出了本文的范围。理解 X.500 标准的要点是,它定义了目录的原则和结构。目录由目录信息树组成,这是一系列服务器或多台服务器上的条目的分层分组。
树中的每个条目都包含一组属性,每个属性有一个或多个值。属性中包含的值可以包含几种数据类型,但最常用的类型是 Unicode 字符串、整数和字节字符串。属性可以存储单个值或多个值。我们将只有一个值的字符串值称为单字符串属性值,例如 cn
值,将具有多个值的字符串值称为多字符串属性值,例如 memberOf
值。
树中的每个条目都包含一个 distinguishedName
属性,该属性定义了条目在目录中的完整位置。distinguishedName
是通过将其相对区分名 (RDN) 与到树根的所有父项的 RDN 连接而得出的。例如,名为 Neal.Bailey 的用户可能有一个 distinguishedName
,即 CN=Neal.Bailey,OU=USERS,OU=HOME,DC=BAILEYSOFT,DC=LOCAL,这清楚地标识了对象在树中的确切位置。使用像 adsiedit.msc 这样的模式查看器可以以可视化的方式演示这一原理。
目录的模式结构在模式中定义,模式定义了可以驻留在目录中的条目类型。理解每个条目的 objectCategory
属性直接链接回定义其类型的模式条目,以及 objectClass
属性定义条目的类型(如组织单位、计算机、用户、组等)非常重要。
当您开始构建有效的 ADO 搜索查询过滤器字符串时,这些细节将变得更加清晰。
轻量级目录访问协议 (LDAP)
轻量级目录访问协议 (LDAP) 是通过 TCP/IP 搜索和修改目录服务的协议。由于 LDAP 是一个标准,任何符合标准的应用程序都可以访问和管理目录。通常,LDAP 服务在端口 389 上运行,用于明文操作,在端口 636 上运行,用于 SSL。该协议通常接受来自客户端的以下类型请求:
- 绑定:认证
- 搜索:定位和检索条目
- 添加:添加新条目
- 删除:移除条目
- 修改:更改条目
- 修改
distinguishedName
:移动/重命名 - 中止:中止进程
- 解绑:关闭连接
对于本文,我们将重点关注搜索功能。
如果您打算使用 Novell.Directory.Ldap 库通过 LDAP (ldaps) 使用 SSL,那么您将需要 Mono.Security.dll。.
LDAP 查询的结构
虽然软件开发的全部目标是抽象(或者说仅公开应用程序调用者所需的成员),但为了构建能够返回预期结果的 LDAP 查询,必须对目录服务有一个基本的了解。如果您跳过了上面的段落,并且对所讨论的主题没有深刻的理解,我建议您在继续之前重新阅读它们。
查询语言是任何系统的重要组成部分,目录服务也不例外。初看之下,它似乎很古老,但一旦您开始执行查询,它就会变得更加清晰。与数据库设计类似,您通常希望在编写一行代码之前,先考虑应用程序将需要的所有不同类型的查询。此外,创建一个简单的控制台应用程序(如本文包含的应用程序)来在将查询集成到解决方案之前进行测试,可能会带来额外的好处。
过滤器中可以包含三种类型的项:运算符、属性和子字符串。
运算符
查询中可以使用四种运算符。有一个等于 (=) 运算符,用于测试相等性。例如:(cn=Neal.Bailey
)。此外,还有范围运算符大于 (>=) 和小于等于 (<=)。例如:(length<=15)。
还有一个近似等于运算符,用于测试近似相等性 (~=)。并非所有实现都支持此功能,因此我将不介绍近似运算符。
属性
您可以在过滤器中指定属性,以确定条目是否具有某个属性。例如,您可以使用等于运算符 (=) 结合通配符星号 (*)。例如,我们有一个名为 ExtensionAttribute3
的自定义属性,因此我们使用 (ExtensionAttribute3=*
) 进行过滤,以返回具有此属性的条目结果集。
子字符串
子字符串提供了匹配具有字符串值的特定条目的能力。这是当今最常见的过滤器类型。在将属性值与字符串匹配时,您可以使用通配符 (*) 星号来表示初始指定字符串之后的任何长度的字符也应匹配。
例如,假设您的目录中有各种位置的安全组,并且您需要了解组织中所有允许用户通过组成员身份进行远程 FTP 访问的组。如果这些组的名称以 FTP 开头(例如,FTP_NRFK_SALES_GS
),则可以指定类似如下的过滤器:(cn=FTP_*
)。此外,如果您使用的是严格的命名约定,那么您甚至可以仅过滤诺福克的 FTP 组(cn=FTP_NRFK_*
)。注意:如果您有其他匹配此字符串的条目,例如用户、打印机、计算机等,它们也将被返回。
此外,您可以使用星号将多个子字符串放在字符串的多个位置。(CN=*NRFK*GS
) 将匹配所有以 CN= 开头,并且 NRFK 字符串后面跟着任何字符序列直到 GS 的条目。
连接过滤器
您也可以在查询中组合多个过滤器。您可以使用与 (&)、竖线 (|) 和感叹号 (!) 运算符连接过滤器。当您组合过滤器时,您可以对查询本身进行相当大的控制。
(&(objectClass=group)(cn=FTP_*))
此过滤器包含两个不同的过滤器:(objectClass=group
) 和 (cn=FTP_*
)。与运算符等同于 AND。因此,此查询匹配 cn
值中包含 FTP_* 的所有组。过滤器用括号括起来的事实表明它是一个单一查询。! 运算符等于 NOT,| 等于 OR。
这些查询可以根据需要变得非常复杂,并且非常灵活。例如,下面您可以看到一个返回 Windows XP 但非 Service Pack 2 的计算机的查询。
(&(&(objectCategory=computer)(!OperatingSystemServicePack=Service Pack 2)
(OperatingSystem=Windows XP Professional)))
除了过滤器,您还需要考虑搜索范围和基点,我将在以下部分进行介绍。
有关查询过滤器标准 RFC1960 的全面分析,请参阅官方文档。
Novell.Directory.Ldap 库
出于示例应用程序的目的,我已经包含了编译后的库,但出于许可原因,请访问 Novell 的网站并下载该库,然后才能将其用于此示例应用程序以外的任何其他用途。Novell 还在其网站上提供了一些文档和示例。由于在此示例中我们专注于搜索目录,因此我们将首先检查 Novell 的示例代码,然后对其进行抽象,使其尽可能易于使用。
using Novell.Directory.Ldap;
using Novell.Directory.Ldap.Utilclass;
private void Search(string ldapHost, //The Directory Server
string loginDN, //The distinguishedName of the account
//with permissions to run this code
string password, //The password for above service account
int ldapPort, //The port to connect to
string searchBase, //The location to start the search
//(dc=baileysoft,dc=com)
string searchFilter) //The query filter
{
try
{
LdapConnection conn = new LdapConnection();
Console.WriteLine("Connecting to:" + ldapHost);
conn.Connect(ldapHost, ldapPort);
conn.Bind(loginDN, password);
LdapSearchResults lsc = conn.Search(searchBase,
LdapConnection.SCOPE_SUB,
searchFilter,
null,
false);
while (lsc.hasMore())
{
LdapEntry nextEntry = null;
try
{
nextEntry = lsc.next();
}
catch (LdapException e)
{
Console.WriteLine("Error: " + e.LdapErrorMessage);
// Exception is thrown, go for next entry
continue;
}
Console.WriteLine("\n" + nextEntry.DN);
LdapAttributeSet attributeSet = nextEntry.getAttributeSet();
System.Collections.IEnumerator ienum = attributeSet.GetEnumerator();
while (ienum.MoveNext())
{
LdapAttribute attribute = (LdapAttribute)ienum.Current;
string attributeName = attribute.Name;
string attributeVal = attribute.StringValue;
if (!Base64.isLDIFSafe(attributeVal))
{
byte[] tbyte = SupportClass.ToByteArray(attributeVal);
attributeVal = Base64.encode(SupportClass.ToSByteArray(tbyte));
}
Console.WriteLine(attributeName + "value:" + attributeVal);
}
}
conn.Disconnect();
}
catch (LdapException e)
{
Console.WriteLine("Error:" + e.LdapErrorMessage);
return;
}
catch (Exception e)
{
Console.WriteLine("Error:" + e.Message);
return;
}
}
方法说明
从上面的 Novell 示例可以看出,他们出色地抽象了使用 LDAP 协议的所有繁琐细节。但是,还需要进一步抽象才能使其可供非 LDAP 专家使用。
参数
ldapHost
:Active Directory、NDS 或 OpenLDAP 提供商(服务器 IP)loginDN
:服务帐户的distinguishedName
,该帐户具有执行此查询的权限password
:上述帐户的密码ldapPort
:连接端口(389)searchBase
:搜索的起点searchFilter
:过滤器查询字符串
重要类型
LdapEntry
- 目录条目LdapConnection
- 连接对象LdapSearchResults
- 结果集合LdapAttributeSet
- 属性集合LdapAttribute
- 特定属性LdapException
- LDAP 错误
有效的抽象
我很难想到有多少企业应用程序不需要目录访问,而且由于大多数组织都不会聘请 LDAP 专家,因此我们需要进一步抽象这段代码,以便在初始设置后,“即插即用”,任何程序员都可以使用它。由于我们的示例应用程序侧重于搜索目录(假设是帮助台应用程序或 Web 应用程序),我们将创建一个名为 search
的类,并针对我们需要为包装器库设计的查询类型。注意:在我部署此类应用程序之前,我会对整个库进行抽象。
抽象 LDAP 设置
我们肯定不能指望普通开发人员了解这些设置,因此我们希望将所有设置存储在一个单独的类中,我们将其命名为 settings.cs。我推测这些值将存储在本地加密的 XML 文件中。对于此示例,这些值在应用程序加载时设置。
namespace Baileysoft.Services.Ldap { public class Settings { public static string Server = null; public static int Port = 389; public static string ServiceAccountDn = null; public static string ServiceAccountPassword = null; public static string SearchBase = null; } }
抽象搜索
为了简洁起见,我不会展示我们搜索类的完整代码,只会展示 ForUser
方法。从这个示例中,您可以看到如何抽象搜索组、目录条目或任何 LDAP 条目。下载源包含更多方法。
....
public static LdapEntry ForUser(string key, string value)
{
return ForLdapEntry(null, null, String.Format("(&(&(objectClass=user)" +
"(!(objectClass=computer)))({0}={1}))", key, value));
}
public static LdapEntry ForUser(string key, string value, string[] PropertiesToLoad)
{
return ForLdapEntry(null, null, String.Format("(&(&(objectClass=user)" +
"(!(objectClass=computer)))({0}={1}))", key, value), PropertiesToLoad);
}
public static LdapEntry ForLdapEntry(string key, string value,
string filter, string[] attributes)
{
LdapEntry searchResult = null;
LdapConnection conn = new LdapConnection();
conn.Connect(Settings.Server, Settings.Port);
conn.Bind(Settings.ServiceAccountDn, Settings.ServiceAccountPassword);
//Search
LdapSearchResults results = conn.Search(Settings.SearchBase, //search base
LdapConnection.SCOPE_SUB, //scope
filter, //filter
attributes, //attributes
false); //types only
while (results.hasMore())
{
try
{
searchResult = results.next();
break;
}
catch (LdapException e)
{
Console.WriteLine(e.Message);
}
}
conn.Disconnect();
return searchResult;
}
....
如您所见,我们有效地封装了现有库的搜索功能,以便团队中所有需要访问目录的成员都可以轻松利用它。
使用新的搜索包装器
有了新的包装器,您就拥有了一套强大的目录搜索工具。让我们来了解一些功能。
设置连接详细信息
我必须再次强调,这应该在应用程序执行时自动完成,并且应源自由目录管理员预先配置的加密 XML 文件。
Settings.Server = "192.168.2.1";
Settings.ServiceAccountDn = "CN=Neal T. Bailey,OU=USERS,OU=HOME,DC=baileysoft,DC=com";
Settings.ServiceAccountPassword = "Cr@zyP@ss";
Settings.SearchBase = "dc=baileysoft,dc=com";
注意:这些方法在用户、组、OU、打印机等之间是可互换的。下面是实际使用包装器的简单演示。
检查条目是否存在
Console.WriteLine(Search.Exists("sAMAccountName", "neal.bailey"));
查找用户
foreach (LdapEntry user in Search.ForUsers())
Console.WriteLine(user.DN);
根据属性值查找用户
if (Search.Exists("sAMAccountName", "neal.bailey"))
{
Console.WriteLine(Search.ForUser("sAMAccountName", "neal.bailey").DN);
}
查找组
if (Search.Exists("cn", "FTP_USERS"))
{
Console.WriteLine(Search.ForGroup("cn", "FTP_USERS").DN);
}
查找组
foreach (LdapEntry group in Search.ForGroups()
Console.WriteLine(group.DN);
根据自定义过滤器查找组
foreach (LdapEntry ftpGroup in Search.ForLdapEntries(null, null,
"(&(objectClass=group)(cn=*FTP*))"))
Console.WriteLine(ftpGroup.DN);
注意:以下方法需要进一步抽象。这些是为了演示属性值枚举.
获取条目的属性值
//Get Single String Attribute Value for User
if (Search.Exists("sAMAccountName", "neal.bailey"))
{
LdapEntry user = Search.ForUser("sAMAccountName", "neal.bailey");
LdapAttribute cn = user.getAttribute("cn");
Console.WriteLine(cn.Name + ": " + cn.StringValue);
}
获取条目的多字符串属性值
//Get Multi-string Attribute Values
if (Search.Exists("sAMAccountName", "neal.bailey"))
{
LdapEntry user = Search.ForUser("sAMAccountName", "neal.bailey");
LdapAttribute members = user.getAttribute("memberOf");
if (members != null)
{
System.Collections.IEnumerator parser = members.StringValues;
while (parser.MoveNext())
{
Console.WriteLine(members.Name + ": " + parser.Current);
}
}
}
加载指定的属性值
//Specify the properties to load on search
if (Search.Exists("sAMAccountName", "neal.bailey"))
{
string[] attribs = { "name", "userPrincipalName", "createTimeStamp" };
LdapEntry user = Search.ForUser("sAMAccountName", "neal.bailey", attribs);
LdapAttributeSet foundAttribs = user.getAttributeSet();
System.Collections.IEnumerator ienum = foundAttribs.GetEnumerator();
while (ienum.MoveNext())
{
LdapAttribute attribute = (LdapAttribute)ienum.Current;
string attributeName = attribute.Name;
string attributeVal = attribute.StringValue;
Console.WriteLine(attributeName + ": " + attributeVal);
}
}
同步与异步搜索
Novell 库包含两种搜索类型的处理。我尚未测试异步搜索。有关代码示例,请参阅 Novell 文档。
SSL 连接
我还没有执行 LDAPS:// 操作的需求,因此无法在此提供帮助。同样,请参阅官方Novell 文档。
基准测试
目前,我还没有足够的时间对该库与 System.DirectoryServices
进行基准测试,因此我无法声称该库比 .NET 库更快。我已经创建了单元测试,并计划在未来几周内进行此测试。
历史
- 提交 - 2007 年 10 月 8 日。