要获得Active Directory中一个对象的值,必须连接Active Directory服务。这个连接过程称为绑定,绑定路径如下所示。
LDAP://dc01.athenaproject.com/OU=Development, DC= AthenaProject, DC=Com
在绑定过程中,可以指定下述内容:
● 指定提供程序使用的协议(protocol)。
● 域控制器的服务器名(server name)。
● 服务器过程的端口号(port number)。
● 对象的显名(distingunshed name),以标识要访问的对象。
● 如果需要访问Active Directory的用户不是运行当前进程的账户,则提供用户名和密码。
● 如果需要加密,应指定authentication类型。
下面详细介绍这些内容。
1. 协议
绑定路径的第一部分指定ADSI提供程序。该提供程序是一个COM服务器;prog-id的标识信息在注册表的HKEY_CLASSES_ROOT下。Windows XP附带的提供程序如表24-2所示。
2. 服务器名
在绑定路径中,服务器名在协议的后面。如果用户登录到Active Directory域上,服务器名就是可选的。如果不提供服务器名,就会发生无服务器绑定操作,此时Windows Server 2003会在域中查找与用户绑定过程相关的、“最好的”域控制器。如果站点中没有服务器,就使用查找到的第一个域控制器。
无服务器的绑定如下所示:
LDAP://OU=Sales,DC=AthenaProject,DC=Local
表 24-2
协 议
|
说 明
|
LDAP
|
LDAP服务器,例如Exchange目录和Windows 2000 Server或Windows Server 2003 Active Directory服务器
|
GC
|
GC用于访问Active Directory中的全局目录。它也可以用于快速查询
|
IIS
|
使用IIS的ADSI提供程序,可以在IIS目录中创建和管理新网站
|
WinNT
|
要访问旧Windows NT 4域的用户数据库,可以使用WinNT 的ADSI提供程序。NT4用户只有几个属性没有改变。也可以使用这个协议绑定Windows 2000域,但这里也限制了可用于NT4的属性
|
NDS
|
这个progID用于和Novell Directory Services通信
|
NWCOMPAT
|
使用NWCOMPAT可以访问旧的Novell目录,例如Novell Netware 3.x
|
3. 端口号
在服务器名的后面,可以指定服务器过程的端口号,其语法是:xxx。LDAP服务器的默认端口号是端口389: LDAP://dc01.globalknowledge.net:389。Exchange服务器使用的端口号与LDAP服务器一样。如果在同一个系统上安装了Exchange服务器,例如用作Active Directory的域控制器,就可以配置另一个端口。
4. 显名
在路径中指定的第四部分是显名(distinguished name,DN)。显名是一个惟一的名称,标识要访问的对象。在Active Directory中,可以使用基于X.500的LDAP语法,指定对象的名称。
例如有一个显名:
CN=Christian Nagel, OU=Consultants, DC= AthenaProject, DC=local
这个显名指定域AthenaProject.local中域组件(Domain Component,DC) AthenaProject的组织单元(Organizational Unit,OU) Consultants的公共名称(Common Name,CN) Christian Nagel。最右边的部分是域的根对象。该名称必须符合对象树中的分层方式。
显名的字符串表示的LDAP规范在 RFC 2253( http://www.ietf.org/rfc/rfc2253.txt)上。
(1) 相对显名
相对显名(RDN)用于引用容器对象中的对象。使用RDN时,不需要指定OU和DC,有一个公共名称就足够了。CN=Christian Nagel就是组织单元中的一个相对显名。如果已经引用了一个容器对象,要访问其子对象,就可以使用相对显名。
(2) 默认的命名环境
如果在路径中没有指定显名,绑定过程就会使用默认的命名环境(default naming context)。使用rootDSE可以读取默认命名环境。LDAP 3.0把rootDSE定义为目录服务器中目录树的根。例如
LDAP://rootDSE
或:
LDAP://servername/rootDSE
通过列举rootDSE的所有属性,将获得没有指定名称时可以使用的defaultNamingContext信息。schemaNamingContext 和 configurationNamingContext指定了用于访问模式所需要的名称和Active Directory库中的配置。
通过下面的代码可获得rootDSE的所有属性:
using (DirectoryEntry de = new DirectoryEntry())
{
de.Path = "LDAP://platinum/rootDSE";
de.Username = @" platinum"christian";
de.Password = "password";
PropertyCollection props = de.Properties;
foreach (string prop in props.PropertyNames)
{
PropertyValueCollection values = props[prop];
foreach (string val in values)
{
Console.Write(prop + ": ");
Console.WriteLine(val);
}
}
}
这个程序显示了默认的命名环境(defaultNamingContext DC=eichkogelstrasse、 DC=local),用于访问模式的环境(CN=Schema、 CN=Configuration、 DC=eichkogelstrasse、 DC=local)和配置的命名环境(CN=Configuration、 DC=eichkogelstrasse、 DC=local),如图24-9所示。
图 24-9
(3) 对象标识符
每个对象都有一个全局惟一的标识符GUID。GUID是一个惟一的128位数字,您可能已经在COM开发中了解了它。可以使用GUID绑定一个对象。这样,即使对象移动到另一个容器中,也可以得到该对象。GUID在创建对象时生成,且总是保持不变。
使用DirectoryEntry.NativeGuid可以得到GUID的字符串表示。这个字符串表示就可以用于绑定对象。
下面的示例显示了一个无服务器绑定的路径名称,它绑定到GUID代表的一个特定对象上:
LDAP://<GUID=14abbd652aae1a47abc60782dcfc78ea>
(4) Windows NT域中的对象名
WinNT提供程序不允许在绑定字符串的名称部分使用LDAP语法。在这个提供程序中,对象用ObjectName、ClassName指定。Windows NT域的有效绑定字符串如下:
WinNT:
WinNT://DomainName
WinNT://DomainName/UserName, user
WinNT://DomainName/ServerName/MyGroup, group
user和group后缀指定可以访问类型为user或group的对象。
5. 用户名
有时,在访问目录时,必须使用一个非当前进程的用户名(也许这个用户没有访问Active Directory所必须的许可),此时必须为绑定过程显式指定用户证书(user credential)。Active Directory提供了许多方式来设置用户名。
(1) Downlevel登录
使用downlevel登录,用户名可以用Windows 2000以前的域名来指定:
domain"username
(2) 显名
也可以用user对象的显名来指定用户,例如:
CN=Administrator, CN=Users,DC=athenaproject,DC=local
(3) User Principal Name (UPN)
对象的UPN用userPrincipalName属性来定义。系统管理员可以在Active Directory Users and Computers工具中User属性的Account选项卡上,用登录信息来指定UPN,注意这不是用户的电子邮件地址。
这些信息也惟一地标识了用户,可以用于登录:
Nagel@ athenaproject.local
6. 身份验证
为了给身份验证进行安全的加密,也可以指定身份验证(authentication)类型。身份验证可以用DirectoryEntry类的AuthenticationType属性设置。可以指定其值为AuthenticationTypes枚举中的一个值。因为枚举是用属性[Flags]标识的,所以可以指定多个值。其可能的值有:对发送的数据进行加密的ReadonlyServer,它指定只需要读取访问,Secure表示安全的身份验证。
7. 用DirectoryEntry类绑定
System.DirectoryServices.DirectoryEntry类可以用于指定所有的绑定信息。可以使用默认的构造函数,用Path、Username、Password和 AuthenticationType属性定义绑定信息,或者把这些信息传递给构造函数:
DirectoryEntry de = new DirectoryEntry();
de.Path = "LDAP://platinum/DC=athenaproject, DC=local";
de.Username = "nagel@ athenaproject.local";
de.Password = "password";
// use the current user credentials
DirectoryEntry de2 = new DirectoryEntry(
"LDAP://DC= athenaproject, DC=local");
即使成功地构造了DirectoryEntry对象,也并不意味着绑定成功了。在第一次读取属性时进行绑定,可以避免不必要的网络流通量。对象是否存在,或者指定的用户证书是否正确,都可以在第一次访问该对象时确定。
前面介绍了如何指定Active Directory中对象的绑定属性,下面要读取对象的属性。在下面的示例中要读取用户对象的属性。
DirectoryEntry类的一些属性可以提供对象的信息,即Name、Guid和 SchemaClassName属性。第一次访问DirectoryEntry对象的属性时,会执行绑定操作,并填充底层ADSI对象的缓存。后面将详细讨论这些。其他属性可以从缓存中读取,同一对象的数据不需要通过与服务器的通信来获得。
在本例中,用组织单元Wrox Press中的公共名称Christian Nagel访问一个用户对象:
using (DirectoryEntry de = new DirectoryEntry())
{
de.Path = "LDAP://platinum/CN=Christian Nagel, " +
"OU=Wrox Press, DC=athenaproject, DC=local";
Console.WriteLine("Name: " + de.Name);
Console.WriteLine("GUID: " + de.Guid);
Console.WriteLine("Type: " + de.SchemaClassName);
Console.WriteLine();
//...
}
Active Directory对象包含许多信息,这些信息取决于对象的类型。属性Properties将返回一个PropertyCollection。每个属性本身就是一个集合,因为一个属性可以有多个值,例如,user对象可以有多个电话号码。在本例中,用一个内部foreach循环查看这些值。从properties[name]返回的集合是一个object数组。属性值可以是字符串、数字或其他类型。使用ToString()方法就可以显示这些值:
Console.WriteLine("Properties: ");
PropertyCollection properties = de.Properties;
foreach (string name in properties.PropertyNames)
{
foreach (object o in properties[name])
{
Console.WriteLine(name + ": " + o.ToString());
}
}
在得到的结果中,包含了user对象Christian Nagel的所有属性,如图24-10所示。OtherTelephone是一个多值属性,它包含许多电话号码。一些属性值只显示System._ComObject对象的类型,例如,lastLogoff、lastLogon和nTSecurityDescriptor。要得到这些属性的值,必须直接使用System.DirectoryServices命名空间的类中的ADSI COM接口。
图 24-10
注意:
第28章介绍了如何使用COM对象和接口。
2. 直接通过名称访问属性
使用 DirectoryEntry.Properties可以访问所有属性。如果已知属性名,就可以直接访问其值:
foreach (string homePage in de.Properties["wWWHomePage"])
Console.WriteLine("Home page: " + homePage);
对象在Active Directory中是分层存储的。容器对象包含子对象。使用DirectoryEntry类的Children属性可以枚举容器中的子对象。另一方面,使用Parent属性可以得到对象的容器。
用户对象没有子对象,所以下面的示例使用一个组织单元,如图24-11所示。非容器对象用Children属性返回一个空集合。下面在域athenaproject.local的组织单元Wrox Press中获得其所有的用户对象。Children属性返回一个DirectoryEntries集合,其中包含DirectoryEntry对象。迭代所有的DirectoryEntry对象,显示子对象的名称。
using (DirectoryEntry de = new DirectoryEntry())
{
de.Path = "LDAP://platinum/OU=Wrox Press, " +
"DC=athenaproject, DC=local";
Console.WriteLine("Children of " + de.Name);
foreach (DirectoryEntry obj in de.Children)
{
Console.WriteLine(obj.Name);
}
}
图 24-11
本例显示了组织单元中的所有对象:users、contacts、printers、shares和其他组织单元。如果只需要查看某些对象类型,可以使用DirectoryEntries类的SchemaFilter属性。SchemaFilter属性返回一个SchemaNameCollection。有了这个SchemaNameCollection,就可以使用Add()方法定义要查看的对象类型。在本例中,因为只需要查看user对象,所以把user添加到这个集合中:
using (DirectoryEntry de = new DirectoryEntry())
{
de.Path = "LDAP:// platinum /OU=Wrox Press, " +
"DC= athenaproject, DC=local";
Console.WriteLine("Children of " + de.Name);
de.Children.SchemaFilter.Add("user");
foreach (DirectoryEntry obj in de.Children)
{
Console.WriteLine(obj.Name);
}
}
结果,只显示组织单元中的user对象,如图24-12所示。
要减少网络流通量,ADSI使用缓存来存储对象属性。如前所述,在创建DirectoryEntry对象时,是不访问服务器的。只要从目录库中读取第一个属性,所有的属性都会写到缓存中,这样,在读取下一个属性时,就不需要往返于服务器了。
写入对象的改变,只会改变已缓存的对象。设置属性不会产生网络流通量。必须使用DirectoryEntry.CommitChanges()才能刷新缓存,把所有已改变的数据传送回服务器。要再次从目录库中获取新写入的数据,可以使用DirectoryEntry.RefreshCache()读取属性。当然,如果没有调用CommitChanges() 和RefreshCache()就修改了一些属性,所有的改变都会丢失,因为我们将再次使用RefreshCache()读取目录服务中的值。
把属性DirectoryEntry.UsePropertyCache设置为false,就可以关闭这个属性cache。但除非在进行调试,否则最好不要关闭它,因为这会与服务器间产生许多不必要的往返通信。
创建新Active Directory对象时,例如users、computers、printers和contacts等,可以使用DirectoryEntries类以编程的方式来完成创建工作。
要给目录添加新对象,首先必须绑定一个容器对象,例如组织单元,可以在其中插入一个新对象。不包含其他对象的对象是不能使用的。下面使用一个容器对象,其显名为CN=Users,DC= athenaproject,DC=local:
DirectoryEntry de = new DirectoryEntry();
de.Path = "LDAP://platinum/CN=Users, DC= athenaproject, DC=local";
使用DirectoryEntry的Children属性,可以得到一个DirectoryEntries对象:
DirectoryEntries users = de.Children;
使用DirectoryEntries,可以添加、删除和查找集合中的对象。下面创建一个新的user对象。使用Add()方法时,需要一个对象名和一个类型名。使用ADSI Edit很容易获得类型名:
DirectoryEntry user = users.Add("CN=John Doe", "user");
对象现在有了默认属性值。要指定特定的属性值,可以使用Properties属性的Add()方法添加属性。当然,所有的属性都必须存在于user对象的模式中。如果指定的属性不存在,就得到一个COMException“指定的目录服务属性或值不存在”:
user.Properties["company"].Add("Some Company");
user.Properties["department"].Add("Sales");
user.Properties["employeeID"].Add("4711");
user.Properties["samAccountName"].Add("JDoe");
user.Properties["userPrincipalName"].Add("JDoe@ athenaproject.local");
user.Properties["givenName"].Add("John");
user.Properties["sn"].Add("Doe");
user.Properties["userPassword"].Add("someSecret");
此时,为了把数据写入Active Directory,必须刷新缓存中的数据:
user.CommitChanges();
24.4.7 更新目录项
Active Directory服务中对象的更新和读取一样简单。可以在读取对象后修改它们的值。要删除一个属性的所有值,可以调用PropertyValueCollection.Clear()方法。使用Add(),可以把新值添加到属性中。Remove()和RemoveAt()可以从属性集合中删除指定的值。
要修改一个值,可以把这个值设置为指定的值。下面的代码将通过PropertyValueCollection的一个索引符把移动电话号码设置为一个新值。使用该索引符,只能改变已存在的值。因此,应使用DirectoryEntry.Properties.Contains()来确定属性是否可用。
using (DirectoryEntry de = new DirectoryEntry())
{
de.Path = "LDAP://platinum /CN=Christian Nagel, " +
"OU=Wrox Press, DC= athenaproject, DC=local";
if (de.Properties.Contains("mobile"))
{
de.Properties["mobile"][0] = "+43(664)3434343434";
}
else
{
de.Properties["mobile"].Add("+43(664)3434343434");
}
de.CommitChanges();
}
在本例的else部分,如果移动电话号码不存在新属性,就用方法PropertyValue Collection.Add()添加它。如果使用Add()方法和已有的属性,得到的结果将取决于属性的类型:单值属性或多值属性。使用Add()方法和已有的单值属性,会得到一个COMException:A constraint violation occurred。使用Add()方法和已有的多值属性则会成功地把另一个值添加到属性中。
User对象的属性mobile定义为单值属性,所以不能添加其他移动电话的号码。但是用户可以有多个移动电话的号码。对于多个移动电话的号码,可以使用属性otherMobile,它是一个多值属性,允许设置多个移动电话号码,所以可以调用Add()多次。多值属性有一个重要的检查:即检查其唯一性。如果把第二个电话号码添加到同一个user对象上,也会得到一个COMException:指定的目录服务属性或值已经存在。
提示:
在创建或更新新的目录对象后,应调用DirectoryEntry.CommitChanges()。否则只能更新缓存中的信息,改变的信息不会发送到目录服务上。
24.4.8 访问内部的ADSI对象
调用预定义的ADSI接口方法常常比搜索对象的属性名容易得多。一些ADSI对象还支持不能在DirectoryEntry类中直接使用的方法。例如IADsServiceOperations接口有启动和停止Windows服务的方法。Windows服务将在第32章讨论。
如本章前面所述,System.DirectoryServices命名空间的类使用底层的ADSI COM对象。DirectoryEntry支持直接使用Invoke()方法调用底层对象的方法。
Invoke()的第一个参数是应在ADSI对象中调用的方法名,第二个参数的params关键字允许把数量可变的其他参数传送给ADSI方法:
public object Invoke(string methodName, params object[] args);
在ADSI文档中介绍了可以用Invoke()方法调用的方法。域中的每个对象都支持IADS接口的方法。前面创建的User对象也支持IADsUser接口的方法。
在下面的代码示例中,使用方法IADsUser.SetPassword()改变前面创建的user对象的密码:
using (DirectoryEntry de = new DirectoryEntry())
{
de.Path = "LDAP://platinum /CN=John Doe, " +
"CN=Users, DC= athenaproject, DC=local";
de.Invoke("SetPassword", "anotherSecret");
de.CommitChanges();
}
如果不使用Invoke(),还可以直接使用底层的ADSI对象。要使用这些对象,必须使用Project | Add Reference添加对Active DS Type Library的引用,如图24-13所示。这会创建一个包装器类,在该类中可以访问ActiveDS命名空间中的这些对象。
图 24-13
内部对象可以使用DirectoryEntry类的NativeObject属性来访问。在下面的示例中,对象de是一个user对象,所以可以把它强制转换为ActiveDs.IADsUser。SetPassword()是在IADsUser接口中说明的方法,所以可以直接调用它,而不是使用Invoke()方法来调用。把IADsUser的AccountDisabled属性设置为false,就可以激活账户。与前面的示例一样,调用CommitChanges() 和DirectoryEntry对象,把改变的内容写到目录服务中:
ActiveDs.IADsUser user = (ActiveDs.IADsUser)de.NativeObject;
user.SetPassword("someSecret");
user.AccountDisabled = false;
de.CommitChanges();
24.4.9 在Active Directory中搜索
Active Directory是一个数据库,它为大多数读取访问进行了优化,所以一般应使用搜索一些值。要在Active Directory中搜索,可以使用.NET Framework中的类DirectorySearcher。
注意:
DirectorySearcher只能和LDAP提供程序一起使用。它不能和NDS或IIS提供程序一起使用。
在类DirectorySearcher的构造函数中,可以定义搜索的4个重要部分。也可以使用默认构造函数,用属性定义搜索选项。
1. SearchRoot
SearchRoot指定搜索应从什么地方开始。SearchRoot的默认值是当前使用的域的根。SearchRoot用DirectoryEntry对象的Path指定。
2. 过滤器
过滤器定义了希望有的值。过滤器是一个必须放在括号中的字符串。
表达式中可以使用关系运算符,如<=、 =、 >=。(objectClass=contact)会搜索所有类型为contact的对象,(lastName>=Nagel) 会按字母表的顺序搜索lastName属性等于或大于Nagel的所有对象。
表达式可以和前缀运算符 & 和 |组合使用。例如,(&(objectClass=user)(description=Auth*))搜索类型为User,其属性description以字符串Auth开头的所有对象。因为 & 和 | 运算符在表达式的开头,所以可以把多个表达式用一个前缀运算符组合在一起。
默认过滤器是(objectClass=*),所以将选择所有的对象。
注意:
过滤器语法在RFC 2254中定义为“LDAP搜索过滤器的字符串表示”。这个RFC在http://www.ietf.org/rfc/rfc2254.txt中。
3. PropertiesToLoad
使用PropertiesToLoad,可以定义所有属性的StringCollection。对象可以有许多属性,其中的大多数对于搜索请求都不太重要。我们定义了应加载到缓存中的属性。如果没有指定属性,默认属性就应是对象的Path 和Name属性。
4. SearchScope
SearchScope 是一个枚举,定义了搜索应延伸的深度:
● SearchScope.Base只搜索对象中的属性,至多可以得到一个对象。
● SearchScope.OneLevel表示在基对象的子集合中继续搜索。基对象本身是不搜索的。
● SearchScope.Subtree定义了在整个树中搜索。
SearchScope属性的默认值是SearchScope.Subtree。
5. 搜索的限制
在目录服务中搜索特定的对象可以在几个域中进行。对搜索要限制对象的个数或搜索时间,可以定义其他几个属性(如表24-3所示)。
表 24-3
属 性
|
说 明
|
ClientTimeout
|
客户机等待服务器返回结果的最长时间。如果服务器没有响应,就不返回记录
|
PageSize
|
使用 paged search,服务器会返回用PageSize 定义的许多对象,而不是所有的对象。这会减少客户获得第一个答案的时间和需要的内存,服务器把一个cookie送给客户机,客户机再把下一个搜索请求发送回服务器,这样搜索就可以在上一次结束的地方继续进行
|
ServerPageTimeLimit
|
对于paged search,这个值定义了一个搜索时间,在该时间内返回用PageSize 值定义的许多对象。如果这段时间在PageSize值之前到达,就会把此时查找到的对象返回给客户机。默认值为-1,表示时间为无限
|
ServerTimeLimit
|
定义服务器搜索对象的最长时间。过了这段时间后,就会把此时查找到的所有对象返回给客户机。默认值是120秒,不能把搜索时间设置为较高的值
|
ReferalChasing
|
搜索可以在几个域中进行。如果用SearchRoot 指定的根是一个父域或没有指定根,就会继续在子域中搜索。使用这个属性可以指定是否应在不同的服务器上继续搜索
ReferalChasingOption.None表示不能在不同的服务器上继续搜索
ReferalChasingOption.Subordinate指定继续在子域中搜索,搜索从DC=Wrox、DC=COM开始时,服务器可以返回一个结果集,例如DC=France、DC=Wrox、DC=COM引用。客户机可以继续在子域中搜索
ReferalChasingOption.External表示服务器可以让客户机搜索不在子域中的另一个服务器,这是默认选项
ReferalChasingOption.All表示返回外部和从属的引用
|
在搜索示例中,要搜索组织单元Wrox Press中description属性值为Author的所有user 对象。
首先,绑定组织单元Wrox Press,在该组织单元中开始搜索。创建一个DirectorySearcher对象,在其中设置SearchRoot。过滤器定义为(&(objectClass=user) (description=Auth*)),这样就可以搜索所有类型为User、description属性以Auth开头的对象。搜索的范围应在一个子树中,即在Wrox Press的子组织单元中搜索:
using (DirectoryEntry de =
new DirectoryEntry("LDAP://OU=Wrox Press, DC= athenaproject, DC=local"))
using (DirectorySearcher searcher = new DirectorySearcher())
{
searcher.SearchRoot = de;
searcher.Filter = "(&(objectClass=user)(description=Auth*))";
searcher.SearchScope = SearchScope.Subtree;
要在搜索结果中包含的属性有name、description、givenName和WWWHomePage。
searcher.PropertiesToLoad.Add("name");
searcher.PropertiesToLoad.Add("description");
searcher.PropertiesToLoad.Add("givenName");
searcher.PropertiesToLoad.Add("wWWHomePage");
下面准备开始搜索。还应对结果进行排序。DirectorySearcher有一个属性Sort,它可以设置SortOption。SortOption构造函数的第一个参数定义了要排序的属性,第二个参数定义了排序的方向。SortDirection枚举的值是Ascending 和 Descending。
要开始搜索,可以使用FindOne()方法查找第一个对象,或者使用FindAll()。FindOne()返回一个简单的SearchResult,FindAll()返回一个SearchResultCollection。要得到所有的作者对象,就应使用FindAll():
searcher.Sort = new SortOption("givenName", SortDirection.Ascending);
SearchResultCollection results = searcher.FindAll();
使用一个foreach循环,可以访问SearchResultCollection中的每个SearchResult。一个SearchResult表示搜索缓存中的一个对象。Properties属性返回一个ResultPropertyCollection,使用属性名和索引符可以访问该集合中所有的属性:
SearchResultCollection results = searcher.FindAll();
foreach (SearchResult result in results)
{
ResultPropertyCollection props = result.Properties;
foreach (string propName in props.PropertyNames)
{
Console.Write(propName + ": ");
Console.WriteLine(props[propName][0]);
}
Console.WriteLine();
}
}
可以在搜索之后获得完整的对象:SearchResult有一个方法GetDirectoryEntry(),它返回查找到的对象的相应DirectoryEntry。
得到的结果应显示《Professional C#》(即《C#高级编程》)的作者列表,这些作者都有我们指定的属性,如图24-14所示。
本章的最后一节要创建一个Windows Forms应用程序UserSearch。这个应用程序是非常灵活的,因为可以输入特定的域控制器、用户名和密码,以访问Active Directory,或者使用运行进程的用户。在这个应用程序中,我们将访问Active Directory服务的模式,获得user对象的属性。该用户可以输入一个过滤器字符串,搜索域中的所有user对象。还可以设置应显示的user对象的属性。
用户界面显示了一些已编好序号的步骤,说明如何使用该应用程序(如图24-15所示):
图 24-15
● 在第一步中,输入用户名、密码和域控制器。所有这些信息都是可选的。如果没有输入域控制器,就要使用无服务器绑定进行连接。如果没有输入用户名,就使用当前用户的安全环境。
● 使用一个按钮,就可以把User对象的所有属性名动态加载到listBoxProperties 列表 框中。
● 在加载了属性名后,就可以选择要显示的属性。列表框的SelectionMode设置为MultiSimple。
● 可以输入限制搜索的过滤器,在该对话框中设置的默认值是搜索所有的user对象:(objectClass=user)。
● 现在开始搜索。
24.5.2 获取模式命名环境
这个应用程序只有两个处理程序方法:按钮的第一个处理程序方法加载属性,第二个处理程序方法则在域中开始搜索操作。在第一部分,从模式中动态读取User类的属性,在用户界面中显示它。
在处理程序方法buttonLoadProperties_Click()中,使用SetLogonInformation()从对话框中读取用户名、密码和主机名,并存储在类的成员中。接着,SetNamingContext()方法设置模式的LDAP名称和默认环境的LDAP名称。这个模式LDAP名称用于调用中,以设置列表框中的属性SetUserProperties()。
private void buttonLoadProperties_Click(object sender, System.EventArgs e)
{
try
{
SetLogonInformation();
SetNamingContext();
SetUserProperties(schemaNamingContext);
}
catch (Exception ex)
{
MessageBox.Show("Check your inputs! " + ex.Message);
}
}
protected void SetLogonInformation()
{
username = (textBoxUsername.Text == "" ? null : textBoxUsername.Text);
password = (textBoxPassword.Text == "" ? null : textBoxPassword.Text);
hostname = textBoxHostname.Text;
if (hostname != "") hostname += "/";
}
在帮助方法SetNamingContext()中,使用目录树的根来获得服务器的属性。这里只考虑两个属性的值:schemaNamingContext 和 defaultNamingContext。
protected string SetNamingContext()
{
using (DirectoryEntry de = new DirectoryEntry())
{
string path = "LDAP://" + hostname + "rootDSE";
de.Username = username;
de.Password = password;
de.Path = path;
schemaNamingContext = de.Properties["schemaNamingContext"][0].ToString();
defaultNamingContext =
de.Properties["defaultNamingContext"][0].ToString();
}
}
使用LDAP名称可以访问模式。使用它可以访问目录,并读取属性。我们不仅要介绍User类的属性,还将介绍User的基类:Organizational-Person、Person和Top。在这个程序中,基类的名称是硬编码的。还可以使用subClassOf属性动态读取基类。GetSchemaProperties()返回一个字符串数组和指定对象类型的所有属性名。这些属性名都在StringCollection属性中:
protected void SetUserProperties(string schemaNamingContext)
{
StringCollection properties = new StringCollection();
string[] data = GetSchemaProperties(schemaNamingContext, "User");
properties.AddRange(GetSchemaProperties(schemaNamingContext,
"Organizational-Person"));
properties.AddRange(GetSchemaProperties(schemaNamingContext, "Person"));
properties.AddRange(GetSchemaProperties(schemaNamingContext, "Top"));
listBoxProperties.Items.Clear();
foreach (string s in properties)
{
listBoxProperties.Items.Add(s);
}
}
在GetSchemaProperties()中,再次访问Active Directory服务。这次不使用rootDSE,而使用前面介绍的模式的LDAP名称。属性systemMayContain包含objectType类中的所有属性的一个集合:
protected string[] GetSchemaProperties(string schemaNamingContext,
string objectType)
{
string[] data;
using (DirectoryEntry de = new DirectoryEntry())
{
de.Username = username;
de.Password = password;
de.Path = "LDAP://" + hostname + "CN=" + objectType + "," +
schemaNamingContext;
DS.PropertyCollection properties = de.Properties;
DS.PropertyValueCollection values = properties["systemMayContain"];
data = new String[values.Count];
values.CopyTo(data, 0);
}
return data;
}
注意上述代码中的DS.PropertyCollection,这是因为在Windows Forms应用程序中,System.DirectoryServices命名空间中的PropertyCollection类与System.Data.PropertyCollection的名称相冲突。为了避免使用像System.DirectoryServices.PropertyCollection这么长的名称,可以使用下面的代码来缩短命名空间的名称:
using DS = System.DirectoryServices;
这样就完成了应用程序的第二步。Listbox控件包含User对象的所有属性名。
搜索按钮的处理程序只调用帮助方法FillResult();
private void buttonSearch_Click(object sender, System.EventArgs e)
{
try
{
FillResult();
}
catch (Exception ex)
{
MessageBox.Show("Check your input: " + ex.Message);
}
}
在FillResult()中,在整个Active Directory 域中进行与前面一样的正常搜索。SearchScope设置为Subtree, Filter设置为TextBox中的字符串,应加载到缓存中的属性由用户在列表框中选择的值来设置。方法GetProperties()用于把属性数组传送给方法searcher。PropertiesTo Load.AddRange()是一个帮助方法,它从列表框中把选中的属性度到一个数组中。在设置了DirectorySearcher对象的属性后,就调用SearchAll()方法搜索属性。SearchResultCollection中的搜索结果用于生成写到文本框textBoxResults中的汇总信息。
protected void FillResult()
{
using (DirectoryEntry root = new DirectoryEntry())
{
root.Username = username;
root.Password = password;
root.Path = "LDAP://" + hostname + defaultNamingContext;
using (DirectorySearcher searcher = new DirectorySearcher())
{
searcher.SearchRoot = root;
searcher.SearchScope = SearchScope.Subtree;
searcher.Filter = textBoxFilter.Text;
searcher.PropertiesToLoad.AddRange(GetProperties());
SearchResultCollection results = searcher.FindAll();
StringBuilder summary = new StringBuilder();
foreach (SearchResult result in results)
{
foreach (string propName in
result.Properties.PropertyNames)
{
foreach (string s in result.Properties[propName])
{
summary.Append(" " + propName + ": " + s + ""r"n");
}
}
summary.Append(""r"n");
}
textBoxResults.Text = summary.ToString();
}
}
}
启动该应用程序,将列出所有由过滤器选择的有效对象,如图24-16所示。
本章介绍了Active Directory的体系结构,有关域、树和森林的重要概念。利用它,我们可以访问整个企业的信息。在编写访问Active Directory服务的应用程序时,必须注意读取的数据可能不是最新的,因为进行复制操作时有一定的等待时间。
使用System.DirectoryServices命名空间中的类,可以很容易地访问封装到ADSI提供程序中的Active Directory服务。DirectoryEntry类可以直接读写数据库中的对象。
使用DirectorySearcher类可以进行复杂的搜索,定义过滤器、超时、加载的属性和范围等。使用全局目录,可以加快对整个企业中的对象的搜索,因为它在森林中存储了所有对象的只读版本。