程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 在.NET Framework 3.5中管理目錄安全主體

在.NET Framework 3.5中管理目錄安全主體

編輯:關於.NET

本文以 Visual Studio 2008 的預發布版為基礎。文中包含的所有信息均有 可能變更。

本文討論:

System.DirectoryServices.AccountManagement 類

Active Directory 域服務

Active Directory 輕型目錄服務 (AD LDS)

管理用戶、計算機和組主體

本文使用了以下技術:

.NET Framework 3.5, Visual Studio 2008

目錄

目錄服務編程體系結構

建立上下文

創建用戶帳戶

創建組和計算機

管理組成員身份

查找 自己

查找匹配項

讓困難的搜索操作變簡單

驗證用戶

擴展性模型

結論

目錄是企業應用程序開發非常重要的組件,但卻很少 有人掌握它。針對 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)。

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 類(和支持類型)。

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 顯示了三種建立上下文的方法。

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=fabrik am,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 方法的等效只讀屬性。

Figure10Other FindBy Methods

方法名稱 只讀屬性 說明 FindByLogonTime LastLogonTime 已在指 定時間內登錄的帳戶。 FindByExpirationTime AccountExpirationDate 指定時間內的過期帳戶。 FindByPasswordSetTime LastPasswordSetTime 在指定時間內被設置了密碼的帳戶。 FindByLockoutTime AccountLockoutTime 在指定時間內被拒絕的帳戶。 FindByBadPasswordAttempt LastBadPasswordAtte mpt 在指定時間內的錯誤密碼嘗試。 沒有等效方法 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 提供建 議。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved