在 .NET Framework 3.5 中管理目录安全主体

System.DirectoryServ.

System.DirectoryServices.AccountManagement 类

  Active Directory 域服务

  Active Directory 轻型目录服务 (AD LDS)

  管理用户、计算 机和组主体

本文使用了以下技术:.NET Framework 3.5, Visual Studio 2008

  

   

  在 .NET Framework 3.5 中管理目录安全主体目录

   目录服务编程体系结构

建立上下文创建用户帐户创建组和计算机管理组成员身份查找自己查找匹配项让困难的搜索操作变简单验证用户扩展性模型结论

   目录是企业应用程序开发非常重要的组件,但却很少有人掌握它。针对 Windows® 平台,Microsoft 提供了三个主要目录平台:Active Directory® 域服务、每台 Windows 计算机上的本地安全帐户管理器 (SAM) 数据存储,以及比较新的 Active Directory 轻型目录服务或 AD LDS(即您先前已经知道的 Active Directory 应用程序模式或简称 ADAM)。虽然大多数的企业开发人员至少了解 SQL 编程的基本知识,但是却很少有人有目录服务编程的经验。

   Microsoft® .NET Framework 的最初版本在 System.DirectoryServices 命名空间内为目录服务编程提供了一组类。这些类是一个简单的托管互操作层,位于现有的基于 COM 的操作系统组件(具体说是 Active Directory 服务接口或 ADSI)之上。这种编程模型具备相当强大的功能,并且,与完整的 ADSI 模型相比,它更为简约通用。

   在 .NET Framework 2.0 中,Microsoft 已将一些功能添加到其 System.DirectoryServices,并提供了两个新的命名空 间:System.DirectoryServices.ActiveDirectory 和 System.DirectoryServices.Protocols。(我们会在整篇文章中分别将其称为 ActiveDirectory 命名空间和 Protocols 命名空间。)ActiveDirectory 命名空间引入了丰富的新类,以便对目录基础结构级组件(例如服务器、域、林、架构和副本)进行强类型化管理。Protocols 命名空间则专攻另一个方向,即为轻型目录访问协议 (LDAP) 编程提供备用 API。这直接与 Windows LDAP 子系统 (wldap32.dll) 配合工作,完全跳过 ADSI COM 互操作层(请参见图 1)。

  在 .NET Framework 3.5 中管理目录安全主体

   Figure 1Microsoft Directory Services Programming Architecture

  开 发人员仍然眷恋 ADSI 中的某些强类型化接口,他们之前将这些接口用于管理安全主体,例如 Users 和 Groups。您可以使用 System.DirectoryServices 中更通用的类来执行大部分此类任务,但是这些操作并不像您想像的那么简单,很多任务都非常模糊。在 .NET Framework 3.5 中,Microsoft 添加了专为管理安全主体设计的新命名空间:System.DirectoryServices.AccountManagement,从而解决了这个问 题。(在本文中我们将 System.DirectoryServices.AccountManagement 称为 AccountManagement 命名空间。)

  这个新命名空间有三个主要目标:简化跨三个目录平台的主体管理操作;不管底层目录为 何,使主体管理操作保持一致;以及为这些操作提供可靠的结果,这样您就不必了解每一个注意事项和每一种特殊情况。

  在等待 .NET 逐步完善的这几年中,Microsoft 通过为利用 NET 功能的这些功能提供更好的 API,同时也对诸如 AD LDS 的新目录平台提供更好的支持,实际上早已胜过了它之前在 ADSI 中的表现。

  目录服务编程体系结构

  在继续之前,先 看一下本文的代码下载,这是为了使用这些技术应具备的前提条件。现在让我们开始。图 1 说明了 System.DirectoryServices 的整体编程体系结构。AccountManagement 命名空间,与 ActiveDirectory 命名空间一样,是位于 System.DirectoryServices 之上的一个抽象层。而 System.DirectoryServices 本身是位于 ADSI 之上的抽象层。AccountManagement 命名空间还依赖于 Protocols 命名空间实现其一小组功能,例如高性能验证。图 1 中深蓝色阴影显示了一部分 AccountManagement 所依赖的目录服务编程体系结构。

  图 2 显示了 AccountManagement 命名空间的重要类型。和添加到 .NET Framework 2.0 中的命名空间不一样,AccountManagement 的外围应用相对较小。除了某些支持类型(例如枚举和异常类)以外,该命名空间会由三个主要的组件组成:Principal 派生类的树,代表强类型化 User、Group 和 Computer 对象;用于建立到底层存储连接的 PrincipalContext 类;以及用于在目录中查找对象的 PrincipalSearcher 类(和支持类型)。

  在 .NET Framework 3.5 中管理目录安全主体

   Figure 2Key Classes in System.DirectoryServices.AccountManagement

   实质上,AccountManagement 命名空间使用一种提供程序设计模式在三个支持的目录平台上进行操作。因此,不管底层目录存储为何,各个 Principal 类的成员的行为方式都类似。此设计对提供简易性和一致性至关重要。

  建立上下文

  您可使用 PrincipalContext 类建立到目标目录的连接,并指定针对该目录执行操作的凭据。此方法与您在 ActiveDirectory 命名空间中使用 DirectoryContext 类建立上下文的方法类似。

  PrincipalContext 构造函数有各种各样的重载,它们提供建立上下文需要的确切选项。如果您已使用过 System.DirectoryServices 中的 DirectoryEntry 类,就会发现很多 PrincipalContext 选项看起来比较眼熟。但是,有三个 PrincipalContext 选项:ContextType、名称和容器,比 DirectoryEntry 类所用的输入参数要特别得多。这种特定性可确保您使用正确的输入参数组合。在 System.DirectoryServices 和 ADSI 中,PrincipalContext 构造函数的这三个参数会被结合到一个称为路径的字符串中。区分这些组件之后,就可以更容易地理解该路径各个部分的意图。

  可使用 ContextType 枚举指定目标目录的类型:Domain(用于 Active Directory 域服务)、ApplicationDirectory(用于 AD LDS)或 Machine(用于本地 SAM 数据库)。相反,当使用 System.DirectoryServices 时,可使用路径字符串(通常是“LDAP”或“WinNT”)的提供程序组件指定目标存储。然后 ADSI 会在内部读取此值以加载合适的提供程序。

  AccountManagement 命名空间中的类可确保您只使用 Framework 支持的提供程序,从而使此任务更简单且更一致。它还避免了 ADSI 和 System.DirectoryServices 编程过程中常见的令人厌烦的提供程序拼写错误和大小写不正确问题。然而,AccountManagement 不支持不太常用的 ADSI 提供程序,例如 IIS 和 Novell Directory Service。

  您可以使用 PrincipalContext 构造函数上的名称参数提供要连接到的特定目录的名称。这可以是某个特定服务器、计算机或域的名称。有一点值得注意,如果此参数为 空,AccountManagement 会试图根据您目前的安全上下文确定用于该连接的默认计算机或域。但是,如果要连接到 AD LDS 存储,则必须为名称参数指定一个值。

  该容器参数允许在目录中指定目标位置以建立上下文。如果使用 Machine ContextType,那么不能指定此参数,因为 SAM 数据库没有层次结构。相反,如果使用 ApplicationDirectory,就必须提供一个值,因为在尝试推断目录根对象时,AD LDS 不发布要使用的 defaultNamingContext 属性。此参数对于 Domain ContextType 是可选的,如果没有指定它,AccountManagement 会使用 defaultNamingContext。

  如有必要,其他 参数(用户名、密码和 ContextOptions 枚举)可让您提供纯文本凭据,并指定要使用的各种连接安全选项。

  所有目录都支持 Windows Negotiate 身份验证方法。如果没有为 Machine 存储指定选项,就会使用 Windows Negotiate 身份验证。但是 Domain 和 ApplicationDirectory 的默认值是具有签名和封条的 Windows Negotiate。

   请注意,Active Directory 域服务和 AD LDS 也支持 LDAP 简单绑定。一般情况下应避免将其用于 Active Directory 域服务,但是如果您希望 AD LDS 存储中的用户执行主体操作,它对于 AD LDS 可能是必需的。

  如果为 用户名或密码参数指定了空值,AccountManagement 会使用当前的 Windows 安全上下文。如果确实要指定凭据,支持的用户名格式是 SamAccountName、UserPrincipalName 和 NT4Name。图 3 显示了三种建立上下文的方法。

  

   在 .NET Framework 3.5 中管理目录安全主体Figure 3 Three Examples of Establishing Context

// create a context for a domain called Fabrikam pointed
// to the TechWriters OU and using default credentials
PrincipalContext domainContext = new PrincipalContext(
ContextType.Domain,"Fabrikam","ou=TechWriters,dc=fabrikam,dc=com");

// create a context for the current machine SAM store with the
// current security context
PrincipalContext machineContext = new PrincipalContext(
ContextType.Machine);

// create a context for an AD LDS store pointing to the
// partition root using the credentials for a user in the AD LDS store
// and SSL for encryption
PrincipalContext ldsContext = new PrincipalContext(
ContextType.ApplicationDirectory, "sea-dc-02.fabrikam.com:50001",
"ou=ADAM Users,o=microsoft,c=us",
ContextOptions.SecureSocketLayer | ContextOptions.SimpleBind,
"CN=administrator,OU=ADAM Users,O=Microsoft,C=US ", "pass@1w0rd01");

   当发生绑定操作时,AccountManagement PrincipalContext 和 System.DirectoryServices DirectoryEntry 类之间存在另一个细微但重要的区别。PrincipalContext 会在创建对象时连接并绑定到底层目录,而 DirectoryEntry 则直到您执行另一个强制连接的操作时才绑定。因此,使用 PrincipalContext 可以立即获得有关连接是否成功绑定到目录的反馈。

创建用户帐户现在您应该非常了解 AccountManagement 是如何使用 PrincipalContext 连接和绑定到容器了。接下来我们将讨论典型的 DirectoryServices 操作——创建用户帐户。在这个过程中,每个代码示例会为必需属性分配一个值、添加两个可选属性、设置密码、启用用户帐户,然后提交对目录的更改。下面我们 使用图 3 示例 1 中引入的 domainContext 变量来创建新的 UserPrincipal:

// create a user principal object
UserPrincipal user = new UserPrincipal(domainContext,
"User1Acct", "pass@1w0rd01", true);

// assign some properties to the user principal
user.GivenName = "User";
user.Surname = "One";

// force the user to change password at next logon
user.ExpirePasswordNow();

// save the user to the directory
user.Save();

  domainContext 可建立到目录的连接和用来执行操作的安全上下文。然后,我们使用一行代码创建新用户对象、设置密码,并启用它。之后,我们使用 GivenName 和 Surname 属性设置底层存储中相应的目录属性。在将对象保存到底层目录存储之前,密码会过期,从而强制用户在第一次登录时更改密码。

做为比 较,图 4 展示了在 System.DirectoryServices 中创建用户帐户所需的等效步骤。第一行代码中的容器变量是 DirectoryEntry 类对象,它为其连接使用一个路径。该路径指定了提供程序、域和容器 (TechWriters OU)。它还使用当前用户的安全上下文进行连接。该容器变量与前一示例中创建用户主体的 domainContext 相似。Figure 4 Create an Account with System.DirectoryServices

  DirectoryEntry container =
new DirectoryEntry("LDAP://ou=TechWriters,dc=fabrikam,dc=com");
// create a user directory entry in the container
DirectoryEntry newUser = container.Children.Add("cn=user1Acct", "user");

// add the samAccountName mandatory attribute
newUser.Properties["sAMAccountName"].Value = "User1Acct";

// add any optional attributes
newUser.Properties["givenName"].Value = "User";
newUser.Properties["sn"].Value = "One";

// save to the directory
newUser.CommitChanges();

// set a password for the user account
// using Invoke method and IadsUser.SetPassword
newUser.Invoke("SetPassword", new object[] { "pAssw0rdO1" });

// require that the password must be changed on next logon
newUser.Properties["pwdLastSet"].Value = 0;

// enable the user account
// newUser.InvokeSet("AccountDisabled", new object[]{false});
// or use ADS_UF_NORMAL_ACCOUNT (512) to effectively unset the
// disabled bit
newUser.Properties["userAccountControl"].Value = 512;

// save to the directory
newUser.CommitChanges();

 

   除了此代码比 AccountManagement 示例稍长一点以外,它还更加复杂。我们需要了解有关该目录中底层数据模型的更多信息,还需要了解如何处理某些帐户管理功能,例如设置初始密码和通过撤消禁 用标志来启用用户对象。如果您必须使用基于反射的调用和 InvokeSet 方法来调用底层 ADSI COM 接口成员,这会比较麻烦。正是因为此复杂性,使得目录服务编程令很多开发人员感到很沮丧。

此外,如果我们尝试在 AD LDS 或本地 SAM 数据库中创建相同的用户帐户,就会有更多不同之处。AD LDS 和 Active Directory 域服务使用不同的机制启用用户帐户(AD LDS 使用 msds-userAccountDisabled 属性,而 Active Directory 域服务使用 userAccountControl 属性),而 SAM 存储则要求调用 ADSI 接口成员。三个目录存储的帐户管理功能缺乏一致性是 AccountManagement 命名空间必须克服的关键设计难题之一。现在,只要更改用来创建 UserPrincipal 对象的 PrincipalContext,即可轻松地在目录存储之间切换,并获得一组一致的帐户管理功能。.NET Framework 2.0 中引入的 Protocols 命名空间并不解决任何这些需要。它的用途是为系统级 LDAP 程序员提供更强大、更灵活的 API,以构建基于 LDAP 的应用程序。但是,与 System.DirectoryServices 相比,它对 LDAP 模型提供的抽象甚至更少,并且对简化不同目录之中的差异没有任何作用。此外,它不适合与本地 SAM 数据库(不是 LDAP 目录)配合使用。本文附带的在线代码示例包含与图 3 后面的 AccountManager 示例相似的示例。它执行相同的任务,但是要使用三倍的代码行。图 5 中的 PrincipalContext 显示了引用某台计算机来定位 SAM 存储的 ContextType 枚举。下一个参数按照名称(或 IP 地址)以实际的计算机为目标,最后两个值提供可创建帐户的帐户凭据。Figure 5 Creating a SAM Database User Account

PrincipalContext principalContext = new PrincipalContext(
ContextType.Machine. "computer01", "adminUser", "adminPassword");

UserPrincipal user = new UserPrincipal(principalContext,
“User1Acct”, “pass@1w0rd01“, true);

//Note the difference in attributes when accessing a different store
//the attributes appearing in IntelliSense are not derived from the
//underlying store
user.Name = "User One";
user.Description = "User One";
user.ExpirePasswordNow();
user.Save();

   我们设置了 Name 和 Description 属性,因为像在 Active Directory 域服务中一样,SAM 存储不包含 givenName、sn 或 displayName 属性。即使 AccountManagement 试图在所有三个目录存储间提供一致的体验,但是底层模型总是会有一些差异。但是,如果您试图获取底层存储中没有的属性,它就会引发 InvalidOperationException。

图 3 和图 5 是创建用户帐户的两个示例;它们显示了在对任何存储执行操作时,AccountManagement 命名空间中固有的一致性编程模型。图 6 中的 PrincipalContext 使用 ContextType.ApplicationDirectory 以 AD LDS 存储为目标,如图 3 所示。下一个参数显示 AD LDS 服务器。在此例中,sea-dc-02.fabrikam.com 是承载 AD LDS 实例的服务器的完全限定域名 (FQDN),该实例侦听用于 SSL 通信的端口 50001。请注意,代码下载在端口 50000 上使用非 SSL 通信。这样并不安全,但是对您自己进行测试没问题。Figure 6 Creating an ADAM or AD LDS User Account

PrincipalContext principalContext = new PrincipalContext(
ContextType.ApplicationDirectory,
"sea-dc-02.fabrikam.com:50001",
"ou=ADAM Users,o=microsoft,c=us",
ContextOptions.SecureSocketLayer | ContextOptions.SimpleBind,
"CN=administrator,OU=ADAM Users,O=Microsoft,C=US",
"P@55w0rd0987");

UserPrincipal user = new UserPrincipal(principalContext,
"User1Acct", "pass@1w0rd01", true);

user.GivenName = "User";
user.Surname = "One";

user.Save();

   下一个参数指定您想在其中执行 CRUD(创建/读取/更新/删除)操作的容器。此示例指定了存储在 AD LDS 中用于执行 CRUD 操作的用户,所以它必须使用 LDAP 简单绑定,并且它出于安全原因它与 SSL 组合在一起。即使 AD LDS 支持本机的安全 DIGEST 验证,但是 ADSI 本身不会。再次重申,我们的示例实际上与前两个示例基本相同,只是 PrincipalContext 有较大的差异。

AccountManagement 命名空间提供了一组全面的帐户管理功能,比如密码过期和帐户解锁。限于篇幅,我们无法在此一一介绍,但最基本点是它们可以跨目录存储一致可靠地发挥作用, 并且省去了实现这些功能的麻烦。创建组和计算机您已发现创建用户帐户很简单,并且可在各个 DirectoryServices 存储之间保持一致。这种一致性可延伸到创建另外两个支持的 DirectoryServices 对象:组和计算机。和 UserPrincipal 类一样,GroupPrincipal 和 ComputerPrincipal 类也是从 Principal 抽象类继承而来,并且操作方法也相似。例如,要在 Active Directory 域服务、AD LDS 或 SAM 帐户数据库中创建名为 Group01 的组,可以使用以下代码:

GroupPrincipal group = new GroupPrincipal(principalContext,
"Group01");
group.Save();

   在每一种情况下,对不同存储建立上下文的 PrincipalContext 类消除了差异。用来创建计算机对象的代码遵循与创建主体对象相似的模式,即使用上下文,然后将对象保存至主体上下文的目标:

ComputerPrincipal computer = new ComputerPrincipal(domainContext);
computer.DisplayName = "Computer1";
computer.SamAccountName = "Computer1$";
computer.Enabled = true;
computer.SetPassword("p@ssw0rd01");
computer.Save();

   再次重申,AccountManagement 会负责使所有受支持标识存储的交互模型达成一致。此例显示了创建可联接到域(它要求 sAMAccountName 属性中有一个尾部 $)的计算机对象的正确语法,但设置了显示名称和公用名,所以不会包括 $。请注意,由于 SAM 数据库和 AD LDS 不包含计算机类,所以 AccountManagement 将只允许在基于域的 PrincipalContext 内部创建此类型的对象。另外,只有 Active Directory 域服务包含各种组作用域:全局、通用和域本地,并包含安全组和通讯组。因此,GroupPrincipal 类会提供可空属性,允许在必要时设置这些值。

管 理组成员身份AccountManagement 命名空间也简化了组成员身份的管理。在 AccountManagement 之前,管理不同存储中的组存在很多特性和不一致。管理具有很多成员的组从编程角度来说非常困难。另外,您必须使用 COM 互操作来管理 SAM 组成员身份,使用 LDAP 属性管理 Active Directory 域服务和 AD LDS 组。但是现在,GroupPrincipal 类的 Members 属性允许您枚举组的成员身份,并管理其成员。一切都迎刃而解。另一个看似简单但实际上难以实现的操作是获取用户所属的所有组。 AccountManagement 提供了好几种方法帮助您。Principal 基类包括两个 GetGroups 方法和两个 IsMemberOf 方法,它们的作用分别是获取任何 Principal 类型的组成员身份,以及查看主体是否是组的成员。同时,UserPrincipal 提供了特殊的 GetAuthorizationGroups 方法,该方法会返回任何 UserPrincipal 类型的完全扩展的安全组成员身份。图 7 显示了您可以如何使用 GetAuthorizationGroups 方法。Figure 7 Using the GetAuthorizationGroups Method

string userName = "user1Acct";

// find the user in the identity store
UserPrincipal user =
UserPrincipal.FindByIdentity(
adPrincipalContext,
userName);

// get the groups for the user principal and
// store the results in a PrincipalSearchResult object
PrincipalSearchResult<Principal> results =
user.GetAuthorizationGroups();

// display the names of the groups to which the
// user belongs
Console.WriteLine("groups to which {0} belongs:", userName);
foreach (Principal result in results)
{
Console.WriteLine("name: {0}", result.Name);
}

   AccountManagement 还简化了另一个有点技巧性的操作,那就是跨受信任的域或使用外部安全主体扩展组成员身份。Principal 类的 GetGroups(PrincipalContext) 方法可以为您承担繁重的工作。

查找自己另一个经常令程序员苦恼的任务是在目录中 查找对象。虽然相对于开发人员日常面对的语法,LDAP 算不上是特别复杂的查询语言,但它仍是一种陌生的语言。而且,即使您了解 LDAP 的基本知识,通常也很难找到使用它执行常见任务的方法。AccountManagement 再一次使这些任务变得非常简单,方法是通过使用 FindByIdentity 方法帮助您查找对象。此方法是 Principal 抽象类的一部分,UserPrincipal、GroupPrincipal 和 ComputerPrincipal 类都是从它继承而来。因此,如果您需要搜索这些主体类的其中一个,FindByIdentity 会是一个值得拥有的好帮手。FindByIdentity 包含两个重载,两个都接受 PrincipalContext 和要查找的值。对于该值,您可以指定任何支持的标识类型:SamAccountName、Name、UserPrincipalName、 DistinguishedName、Sid 或 Guid。第二个重载还允许您显式定义即将指定为该值的标识类型。从相对简单的重载开始,以下是着手使用 FindByIdentity 的方法,目标是返回我们在前一示例中创建的用户帐户:

UserPrincipal user = UserPrincipal.FindByIdentity(principalContext, "user1Acct");

   一旦有了上下文(存储在 principalContext 对象中),就可以使用 FindByIdentity 方法检索主体对象,在本例中是 UserPrincipal。建立了任何受支持的标识存储的上下文后,用于查找该标识的代码始终都一样。

第二个 FindByIdentity 构造函数允许您显式指定标识格式。使用此构造函数时,必须使该值的格式与指定的标识类型相匹配。例如,以下代码使用 UserPrincipal 的可分辨名称正确返回它,前提是该对象存在于目录中并且位于指定的位置:

UserPrincipal user = UserPrincipal.FindByIdentity(
adPrincipalContext,
IdentityType.DistinguishedName,
"CN=User1Acct,OU=TechWriters,DC=FABRIKAM,DC=COM");

  相反,以下代码不会返 回 UserPrincipal,因为 IdentityType 枚举指定了 DistinguishedName 格式,但是该值没有使用此格式:

UserPrincipal user = UserPrincipal.FindByIdentity(
adPrincipalContext,
IdentityType.DistinguishedName,
"user1Acct");

  格式很重要。例如,如 果您决定使用 GUID 或 SID 标识类型,就必须对该值分别使用标准的 COM GUID 格式和安全描述符描述语言 (SDDL) 格式。本文的代码下载提供了两种显示正确格式的方法(FindByIdentityGuid 和 FindByIdentitySid)。请注意,您必须更改这些方法中的 GUID 或 SID 值,以便在目录存储中找到匹配项。正如我们稍后要介绍的那样,使用 PrincipalSearcher 类获得其中任何一个格式都非常简单。

现 在您已找到 Principal 对象,并与其绑定,接下来就可以轻松地对它执行操作了。例如,您可以将用户添加到组,如下所示:

// get a user principal
UserPrincipal user =
UserPrincipal.FindByIdentity(adPrincipalContext, "User1Acct");
// get a group principal
GroupPrincipal group =
GroupPrincipal.FindByIdentity(adPrincipalContext, "Administrators");
// add the user
group.Members.Add(user);
// save changes to directory
group.Save();

  在这里,我们使用 FindByIdentity 方法先找到用户,然后再找到组。一旦代码获得这些主体对象,我们就会调用组的 Members 属性的 Add 方法,将该用户主体添加到该组。最后,我们调用组的 Save 方法以保存对目录的更改。

查找匹配项您还可以使用强大的示例查询 (QBE) 功能和 PrincipalSearcher 类,根据定义好的条件查找对象。我们会对 QBE 和 the PrincipalSearcher 类进行更详细的说明,但是我们先看一下简单的搜索示例。图 8 显示了如何查找所有 name/cn 前缀以“user”开头且禁用的用户帐户。Figure 8 Using PrincipalSearcher

// create a principal object representation to describe
// what will be searched
UserPrincipal user = new UserPrincipal(adPrincipalContext);

// define the properties of the search (this can use wildcards)
user.Enabled = false;
user.Name = "user*";

// create a principal searcher for running a search operation
PrincipalSearcher pS = new PrincipalSearcher();

// assign the query filter property for the principal object
// you created
// you can also pass the user principal in the
// PrincipalSearcher constructor
pS.QueryFilter = user;

// run the query
PrincipalSearchResult<Principal> results = pS.FindAll();

Console.WriteLine("Disabled accounts starting with a name of 'user':");
foreach (Principal result in results)
{
Console.WriteLine("name: {0}", result.Name);
}

   PrincipalContext 变量(adPrincipalContext)指向 Active Directory 域,但是指向 AD LDS 应用程序分区也同样简单。建立上下文以后,请注意该代码会创建新的 UserPrincipal 对象。这是用于搜索操作的主体在内存中的表示形式。一旦创建了这个主体,然后就要设置限制搜索结果的属性。接下来的两行代码显示了可以设置的一些限制—— 用户名以某些值开头的所有禁用用户帐户。请注意,Name 属性的属性值支持通配符。

如果您已熟悉用于设置搜索筛选器的 LDAP 语言,立刻就会发现 QBE 为何是如此新颖且更加直观的替代选择。有了 QBE 之后即可设置后来用于查询操作的示例对象。为了清楚地证明 QBE 比典型的 DirectoryServices 搜索语言简单,以下显示的 LDAP 语言可以设置与图 8 中创建的 QBE 对象等效的筛选器:

(&(objectCategory=person)(objectClass=user)(name=user*)(userAccount
Control:1.2.840.113556.1.4.803:=2))

   如您所见,LDAP 语言要复杂得多,并且不适用于 AD LDS,因为 Active Directory LDS 用户架构使用 msDS-UserAccountDisabled 属性,而不是 LDAP 语言中显示的 userAccountControl 属性。同样,AccountManagement 会为我们在幕后处理这些差异。

设置了图 8 显示的 QBE 对象后,我们可以创建 PrincipalSearcher 对象,并指定它的 QueryFilter 属性,这是 Principal 对象之前在代码中创建的。请注意,您还可以在 PrincipalSearcher 构造函数中传递用户主体,而不是设置 QueryFilter 属性。然后运行查询,即调用 PrincipalSearcher 的 FindAll 方法,并将返回的结果分配给 PrincipalSearchResult 通用列表。PrincipalSearchResult 列表会存储返回的 Principal 对象。最后,代码枚举主体列表,并显示每个返回主体的 Name 属性。请注意,QBE 不适用于引用属性。也就是说,不归 QBE 所有的属性不能用来配置对象在内存中的表示形式。在 foreach 循环中可以做更多的事情。例如启用禁用的用户帐户或删除它们。如果您只对读取操作感兴趣,请记住如果您确实指向某个其他的标识存储,那么返回的属性就必须 存在于该存储中。例如,因为 AD LDS 用户不包含 sAMAccountName 属性,尝试在结果中返回此属性就会毫无意义。让困难的搜索操作变简单另外,还有其他一些强大的 FindBy 方法,当结合 PrincipalSearchResult 类时,这些方法可以检索通常难以检索的用户和计算机主体的相关信息。图 9 演示了如何检索当天更改其密码的每个用户的名称。本示例使用了 FindByPasswordSetTime 方法和 PrincipalSearchResult 类。如果没有 AccountManagement,此操作会很复杂,因为底层 pwdLastSet 属性是作为大整数存储在目录中。Figure 9 Retrieving Users Who Reset Their Password Today

// get today's date
DateTime dt = DateTime.Today;

// run a query
PrincipalSearchResult<Principal> results =
UserPrincipal.FindByPasswordSetTime(
adPrincipalContext,
dt,
MatchType.GreaterThanOrEquals);

Console.WriteLine("users whose password was set on {0}",
dt.ToShortDateString());
foreach (Principal result in results)
{
Console.WriteLine("name: {0}", result.Name);
}

  本文的代码下载包含使用其他 FindBy 方法的示例。它们的操作方法与我们在图 9 中显示的类似。

  FindBy 方法是获取通常难以检索的信息的捷径。但是,如果需要使用 QBE 工具进一步筛选结果,那它们就不太适合。此处有一个重要的细微差别,那就是相关属性是只读的,因此不能在 QBE 对象上设置,就像不能由用户在 QBE 引用的对象上设置一样。为了使用 QBE,您可以使用示例主体对象中的等效只读属性,并与 AdvancedSearchFilter 属性结合使用。有关此操作的详细信息稍后再做说明。图 10 列出了更多的 FindBy 方法,并显示了可以在搜索中代替 FindBy 方法的等效只读属性。

  

  在 .NET Framework 3.5 中管理目录安全主体Figure10Other FindBy Methods

  

方法名称 只读属性 说 明
FindByLogonTime LastLogonTime 已在指定时间内 登录的帐户。
FindByExpirationTime AccountExpirationDate 指 定时间内的过期帐户。
FindByPasswordSetTime LastPasswordSetTime 在指定时间内被设置了密码的帐户。
FindByLockoutTime AccountLockoutTime 在 指定时间内被拒绝的帐户。
FindByBadPasswordAttempt LastBadPasswordAttempt 在 指定时间内的错误密码尝试。
没有等效方法 BadLogonCount 尝试在指 定次数内登录但是登录失败的帐户。

  配置 QBE 时不能设置只读属性的值。那么在搜索操作中如何使用该属性呢?您可以先检索结果集,然后在枚举该结果集时使用只读属性执行条件测试。但请记住,建议不要将 该方法用于可能较大的结果集,因为该代码必须先检索未为只读属性筛选的结果,然后再筛选只读属性返回的结果。代码下载中的 PrincipalSearchEx6v2 方法演示了这个并非完美的方法。

Directory Services 团队通过将 AdvancedSearchFilter 属性添加到 AuthenticablePrincipal 类解决了这个 QBE 局限性。AdvancedSearchFilter 允许根据只读属性进行搜索,然后将它们与其他可使用 QBE 机制设置的属性结合起来。图 11 演示了如何使用 UserPrincipal 类的 LastBadPasswordAttempt 只读属性,返回当天输入过无效密码的用户的列表。Figure 11 AdvancedSearchFilter with a Read-Only Property

DateTime dt = DateTime.Today;

// create a principal object representation to describe
// what will be searched
UserPrincipal user = new UserPrincipal(adPrincipalContext);

user.Enabled = true;

// define the properties of the search (this can use wildcards)
user.Name = "*";

//add the LastBadPasswordAttempt >= Today to the query filter
user.AdvancedSearchFilter.LastBadPasswordAttempt
(dt, MatchType.GreaterThanOrEquals);

// create a principal searcher for running a search operation
// and assign the QBE user principal as the query filter
PrincipalSearcher pS = new PrincipalSearcher(user);

// run the query
PrincipalSearchResult<Principal> results = pS.FindAll();

Console.WriteLine("Bad password attempts on {0}:",
dt.ToShortDateString());
foreach (UserPrincipal result in results)
{
Console.WriteLine("name: {0}, {1}",
result.Name,
result.LastBadPasswordAttempt.Value);
}

  验证用户

构建基于目录 的应用程序的开发人员通常需要验证存储在目录中的用户的凭据,尤其是在使用 AD LDS 时。在 .NET Framework 3.5 之前,程序员使用 System.DirectoryServices 中的 DirectoryEntry 类在内部强制执行 LDAP 绑定操作,从而完成这个任务。但是,请注意:这种情况极有可能发生,即这种版本的代码不安全、速度慢或只能带来麻烦。另外,ADSI 本身并不是专为这类型的操作而设计的,它无法在使用率较高的情况下运行,这归咎于它在内部缓存 LDAP 连接的方式。正如我们已讨论过的那样,.NET Framework 2.0 中的 System.DirectoryServices.Protocols 程序集包含了较低级别的 LDAP 类,它们使用基于连接的编程方法。此设计让您可以克服 ADSI 中的固有局限,但代价是必须编写更复杂的代码。在 .NET Framework 3.5 中,AccountManagement 为所有环境中工作的程序员提供了 ASP.NET 中的 ActiveDirectoryMembershipProvider 实现所带来的功能和易用性。另外,如有需要,AccountManagement 命名空间还允许针对本地 SAM 数据库验证凭据。PrincipalContext 类的两个 ValidateCredentials 方法都提供了凭据验证。首先使用想验证的目录创建 PrincipalContext 实例,然后再指定合适的选项。获得上下文后,可以根据提供的用户名和密码值测试 ValidateCredentials 返回的是 true 还是 false。图 12 显示了在 AD LDS 中验证用户的示例。Figure 12 Authenticating a User in AD LDS

// establish context with AD LDS
PrincipalContext ldsContext =
new PrincipalContext(
ContextType.ApplicationDirectory,
"sea-dc-02.fabrikam.com:50000",
"ou=ADAM Users,O=Microsoft,C=US");

// determine whether a user can validate to the directory
Console.WriteLine(
ldsContext.ValidateCredentials(
"user1@adam",
"Password1",
ContextOptions.SimpleBind +
ContextOptions.SecureSocketLayer));

  当您想快速有效地验证很多不同的用户凭据集 时,该方法最有用。您为相关目录存储创建单个 PrincipalContext 对象,并在对 ValidateCredentials 的每一次调用中重用该对象。PrincipalContext 可以重用到目录的连接,这会带来良好的性能和可伸缩性。对 ValidateCredentials 的调用是线程安全调用,所以您的实例可在此操作的线程间使用。有一点必须注意,对 ValidateCredentials 的调用不会更改用来创建 PrincipalContext 的凭据——上下文和方法调用会保持单独的连接。

默 认情况下,AccountManagement 使用安全的 Windows Negotiate 身份验证,并在对 AD LDS 执行简单绑定时试图使用 SSL。我们建议您最好始终指明想执行的验证类型和想使用的连接保护(如果适用),但至少是默认错误(为了保险起见)。Windows Server® 2003 及更高版本中的 Active Directory 域服务以及 AD LDS 都包括快速并发绑定,它们是专为高性能验证操作而设计的。它可以在不实际上为用户构建安全令牌的情况下验证用户密码。与普通绑定操作不一样,使用快速并发 绑定时,LDAP 连接的状态会保持未绑定。您可使用快速并发绑定对同一个连接重复执行绑定操作,而且只需查看失败的密码尝试即可。此功能在 ADSI 或 System.DirectoryServices 中不是可用的选项,但在 Protocols 命名空间中已公开为选项。只要可能,AccountManagement 就会使用快速并发绑定,并自动启用此选项。这就是在图 1 中 AccountManagement 层也显示在 Protocols 层上方的原因。请注意它只适用于简单绑定模式,该模式在网络中传递纯文本凭据。因此,为了安全起见,快速并发绑定应始终与 SSL 结合使用。AccountManagement 另一个真正的亮点是它的扩展性模型。很多开发人员都会选择使用各种 Principal 派生的类来构建 Active Directory 域服务和 AD LDS 的自定义配置系统。在很多情况下(特别是使用 AD LDS 时),组织会将自定义架构扩展添加到目录,以支持它自己的用户和组元数据。使用 .NET Framework 面向对象的设计和基于属性的可扩展元数据,AccountManagement 使得创建支持您的自定义架构的自定义安全主体类变得非常简单。只要从 Principal 派生的类之一继承,并使用适当的属性标记您的类和属性,您的自定义主体类就可以读取和写入这些目录属性以及已受内置类型支持的属性。值得注意的重要细微差 别是,AccountManagement 提供的扩展性机制专供存储在 Active Directory 域服务或 AD LDS 中的安全主体使用。它的重点不在于非 Microsoft LDAP 目录。如果您想为非 Microsoft LDAP 目录中的配置构建框架,则应在 Protocols 命名空间中使用级别较低的类。(此外,扩展性模型并不适合用于本地 SAM 帐户,因为 SAM 架构不可扩展。)请考虑使用标准 LDAP 用户类来存储应用程序安全主体的 AD LDS 目录。此外,LDAP 目录架构已扩展为支持一个特殊属性,可标识称为 msdn-subscriberID 的用户对象。图 13 演示了如何创建可配置用户对象并针对此属性提供创建、读取和写入操作的自定义类。Figure 13 Our Sample MsdnUser Class

[DirectoryObjectClass("user")]
[DirectoryRdnPrefix("CN")]
class MsdnUser : UserPrincipal
{
public MsdnUser(PrincipalContext context)
: base(context) { }

  public MsdnUser(
PrincipalContext context,
string samAccountName,
string password,
bool enabled
)
: base(
context,
samAccountName,
password,
enabled
)
{
}

  [DirectoryProperty("msdn-subscriberID")]
public string MsdnSubscriberId
{
get
{
object[] result = this.ExtensionGet("msdn-subscriberID");
if (result != null) {
return (string)result[0];
}
else {
return null;
}
}
set { this.ExtensionSet("msdn-subscriberID", value); }
}
}

   请注意,该代码从 UserPrincipal 类派生而来,并配置了两个属性:DirectoryObjectClass 和 DirectoryRdnPrefix。这两个属性都是主体扩展类必需的。在目录中创建此对象的实例时,DirectoryObjectClass 属性会确定受支持的存储(Active Directory 域服务或 AD LDS)用于 objectClass 目录属性的值。这里,它仍然是默认的 AD LDS 用户类,但是实际上它可以是任何类。DirectoryRdnPrefix 属性确定在目录中为此类的对象命名所使用的 RDN(相对可分辨名称)属性名称。在 Active Directory 域服务下不能更改 RDN 前缀——对于安全主体类,它始终为 CN。不过在 AD LDS 下就比较灵活,如果需要,可以使用不同的 RDN。

  我们的类有一个称 为 MsdnSubscriberID 的属性,它返回一个字符串。这个类标有 DirectoryProperty 属性,指定用于存储属性值的 LDAP 架构属性。底层框架使用此值针对 Principal 类型优化搜索操作。

  我们的属性 get 和 set 实现使用 Principal 基类的受保护 ExtensionGet 和 ExtensionSet 方法,将值读取和写入底层属性缓存。这些方法支持将尚未保留到数据库/标识存储中的对象的值存储到内存中。此外,这些方法还支持从现有的对象读取和写入 值。由于 LDAP 目录支持各种类型的属性,也允许一个属性包含多个值,所以这些方法使用 object[] 类型来读取和写入值。这种灵活性非常有用,但是如果想在对象类型阵列之上提供强类型化的标量字符串值,就必须多做一些额外的工作,如我们的实现所示。对于 我们自定义 MsdnUser 类的使用者来说,其结果就是一个非常容易编程的接口。

  在目录架构之上提供强类型化的值这种功能是此扩展 性模型最有用的功能之一。除了简单的字符串类型以外,您还可以使用 .NET Framework 提供的丰富类型系统来做一些事情,例如将 Active Directory 域服务的 jpgPhoto 属性表示成 System.Drawing.Image 或 System.IO.Stream,而不是您通常通过从 System.DirectoryServices 读取值而获得的默认 byte[]。

   本文中的代码下载提供了更多的示例来演示这些功能。它还包括一些架构扩展(通过标准的 LDIF 格式化文件 msdnschema.ldf),这些架构扩展可用来扩展具有 MsdnUser 类的测试目录。我们还在“目录服务资源”边栏提供了一些很有价值的链接。

  结论

  Microsoft 提供了丰富的目录服务编程模型,AccountManagement 是对其必不可少的托管代码补充。凭借 AccountManagement 命名空间,开发人员现在就拥有了一组可用于常见 CRUD 和搜索操作的强类型化主体。

  命名空间封装了目录服务编程最佳做法,可帮助您 编写安全且高性能的托管代码。此外,AccountManagement 是可扩展的,允许您在 Active Directory 域服务和 AD LDS 中与自定义目录对象全面交互。

  

  Joe Kaplan 在 Accenture 的内部 IT 组织工作,该组织使用 .NET Framework 构建企业应用程序。他擅长于应用程序安全性、联合身份管理和目录服务编程,因此被公认为 Microsoft MVP。他是“The .NET Developer’s Guide to Directory Services Programming”(目录服务编程:.NET 开发人员指南)(Addison-Wesley, 2006) 的合著者。

   Ethan Wilansky 是一位 Microsoft 目录服务 MVP 和 EDS 企业架构师。作为 EDS 创新工程实践的一部分,他目前带领一个开发团队,专注于构建自定义的 SharePoint 应用程序,并就目录服务编程解决方案向 EDS 提供建议。