本文基於 Visual Studio 2008 預發布版本撰寫,文中資訊可能有所變更。
本文深入探討 System.DirectoryServices.AccountManagement 命名空間,涵蓋 Active Directory 域服務 (AD DS)、Active Directory 輕型目錄服務 (AD LDS),以及如何管理使用者、電腦和群組主體。相關技術包括 .NET Framework 3.5 和 Visual Studio 2008。
目錄是企業應用程式開發的關鍵組件,但鮮少開發者精通此道。微軟在 Windows 平台提供三大目錄平台:Active Directory 域服務、本地安全帳戶管理器 (SAM) 資料存儲,以及 Active Directory 輕型目錄服務 (AD LDS,舊稱 ADAM)。雖然大多數企業開發者熟悉 SQL 程式設計,但目錄服務程式設計經驗卻相對稀缺。
.NET Framework 初版在 System.DirectoryServices 命名空間提供目錄服務程式設計類別,這是一個建立在 COM 架構的 ADSI (Active Directory 服務介面) 之上的簡單托管互操作層。雖然功能強大,但與完整 ADSI 模型相比較為精簡。
.NET Framework 2.0 擴充了 System.DirectoryServices,並新增兩個命名空間:System.DirectoryServices.ActiveDirectory (ActiveDirectory 命名空間) 和 System.DirectoryServices.Protocols (Protocols 命名空間)。前者提供豐富類別管理目錄基礎結構組件 (伺服器、域、樹系、架構、副本);後者直接與 Windows LDAP 子系統 (wldap32.dll) 配合,繞過 ADSI COM 互操作層,提供輕量級 LDAP 程式設計 API。
開發者仍偏愛 ADSI 中用於管理安全主體 (使用者、群組) 的強類型介面。雖然 System.DirectoryServices 的通用類別能執行大部分任務,但操作並不直觀,許多細節模糊不清。.NET Framework 3.5 新增專為管理安全主體設計的 System.DirectoryServices.AccountManagement 命名空間 (本文簡稱 AccountManagement 命名空間),徹底解決此問題。
AccountManagement 命名空間採用提供者設計模式,在三大支援目錄平台上操作,確保各 Principal 類別成員行為一致,這是實現簡易性與一致性的關鍵。
使用 PrincipalContext 類別建立到目標目錄的連線,並指定操作憑證,類似 ActiveDirectory 命名空間中的 DirectoryContext。其建構函數提供多種重載,三個關鍵參數 ContextType、名稱、容器,比 DirectoryEntry 的路徑字串更具體、易懂。
Domain:Active Directory 域服務ApplicationDirectory:AD LDSMachine:本地 SAM 資料庫名稱參數指定特定目錄 (伺服器、電腦或域名);容器參數指定目錄中目標位置。所有目錄支援 Windows Negotiate 驗證,Domain 和 ApplicationDirectory 預設使用帶簽章與密封的 Negotiate。
// 建立指向 Fabrikam 域 TechWriters OU 的上下文,使用預設憑證
PrincipalContext domainContext = new PrincipalContext(
ContextType.Domain, "Fabrikam", "ou=TechWriters,dc=fabrikam,dc=com");
// 建立當前電腦 SAM 存儲上下文,使用當前安全上下文
PrincipalContext machineContext = new PrincipalContext(ContextType.Machine);
// 建立 AD LDS 存儲上下文,指向分區根,使用 AD LDS 使用者憑證與 SSL
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");
PrincipalContext 在建立物件時即連線並繫結到底層目錄,可立即獲得連線成功與否的回饋,這與 DirectoryEntry 延遲繫結行為不同。
使用 UserPrincipal 建立使用者帳戶極為簡潔:
// 建立使用者主體物件,一行程式碼設定密碼並啟用
UserPrincipal user = new UserPrincipal(domainContext, "User1Acct", "pass@1w0rd01", true);
// 設定屬性
user.GivenName = "User";
user.Surname = "One";
// 強制使用者下次登入時變更密碼
user.ExpirePasswordNow();
// 儲存至目錄
user.Save();
對比 System.DirectoryServices 同等操作 (圖 4),後者程式碼較長且複雜,需了解底層資料模型、處理密碼設定與帳戶啟用等細節,甚至需透過反射呼叫 ADSI COM 介面。更關鍵的是,三大目錄存儲 (AD DS、AD LDS、SAM) 的帳戶管理機制各異,AccountManagement 透過統一程式設計模型屏蔽了這些差異,只需更改 PrincipalContext 即可在目錄間切換。
PrincipalContext principalContext = new PrincipalContext(
ContextType.Machine, "computer01", "adminUser", "adminPassword");
UserPrincipal user = new UserPrincipal(principalContext, "User1Acct", "pass@1w0rd01", true);
// SAM 存儲無 givenName、sn、displayName 屬性,使用 Name 與 Description
user.Name = "User One";
user.Description = "User One";
user.ExpirePasswordNow();
user.Save();
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();
GroupPrincipal 和 ComputerPrincipal 同樣繼承自 Principal 抽象類別,操作模式一致:
// 建立群組
GroupPrincipal group = new GroupPrincipal(principalContext, "Group01");
group.Save();
// 建立電腦物件 (需尾部 $)
ComputerPrincipal computer = new ComputerPrincipal(domainContext);
computer.DisplayName = "Computer1";
computer.SamAccountName = "Computer1$";
computer.Enabled = true;
computer.SetPassword("p@ssw0rd01");
computer.Save();
注意:SAM 和 AD LDS 不包含電腦類別,故 ComputerPrincipal 僅能在域類型 PrincipalContext 中建立。GroupPrincipal 提供可為 null 的屬性設定群組範圍 (全域、通用、域本地) 和類型 (安全性群組、通訊群組)。
GroupPrincipal.Members 屬性大幅簡化群組成員管理。以往需分別處理 SAM (COM 互操作)、AD DS/AD LDS (LDAP 屬性),如今統一介面輕鬆搞定。
取得使用者所屬所有群組:
Principal.GetGroups():取得任何主體類型的群組成員資格Principal.IsMemberOf():檢查主體是否為群組成員UserPrincipal.GetAuthorizationGroups():取得使用者完整展開的安全性群組成員資格 (含巢狀群組)string userName = "user1Acct";
UserPrincipal user = UserPrincipal.FindByIdentity(adPrincipalContext, userName);
PrincipalSearchResult<Principal> results = user.GetAuthorizationGroups();
Console.WriteLine("groups to which {0} belongs:", userName);
foreach (Principal result in results)
{
Console.WriteLine("name: {0}", result.Name);
}
跨受信任域或使用外部安全主體擴展群組成員資格,可使用 Principal.GetGroups(PrincipalContext) 方法。
FindByIdentity 方法支援多種身分識別類型:SamAccountName、Name、UserPrincipalName、DistinguishedName、Sid、Guid。兩個重載分別為隱含與顯式指定識別類型。
// 簡單查詢
UserPrincipal user = UserPrincipal.FindByIdentity(principalContext, "user1Acct");
// 顯式指定識別類型
UserPrincipal user = UserPrincipal.FindByIdentity(
adPrincipalContext,
IdentityType.DistinguishedName,
"CN=User1Acct,OU=TechWriters,DC=FABRIKAM,DC=COM");
找到物件後可直接操作,例如將使用者加入群組:
UserPrincipal user = UserPrincipal.FindByIdentity(adPrincipalContext, "User1Acct");
GroupPrincipal group = GroupPrincipal.FindByIdentity(adPrincipalContext, "Administrators");
group.Members.Add(user);
group.Save();
強大的 QBE 功能配合 PrincipalSearcher,可依條件查詢物件。建立範例物件、設定屬性作為查詢條件、執行搜尋:
UserPrincipal user = new UserPrincipal(adPrincipalContext);
user.Enabled = false;
user.Name = "user*"; // 支援萬用字元
PrincipalSearcher pS = new PrincipalSearcher();
pS.QueryFilter = user;
PrincipalSearchResult<Principal> results = pS.FindAll();
foreach (Principal result in results)
{
Console.WriteLine("name: {0}", result.Name);
}
對比同等 LDAP 查詢語法:
(&(objectCategory=person)(objectClass=user)(name=user*)(userAccountControl:1.2.840.113556.1.4.803:=2))
QBE 直觀得多,且 AccountManagement 自動處理不同目錄間的屬性差異 (如 AD LDS 使用 msDS-UserAccountDisabled 而非 userAccountControl)。
針對難以透過 QBE 查詢的唯讀屬性 (如 pwdLastSet 大整數),提供專用 FindBy 方法:
DateTime dt = DateTime.Today;
PrincipalSearchResult<Principal> results = UserPrincipal.FindByPasswordSetTime(
adPrincipalContext, dt, MatchType.GreaterThanOrEquals);
foreach (Principal result in results)
{
Console.WriteLine("name: {0}", result.Name);
}
| 方法名稱 | 唯讀屬性 | 說明 |
|---|---|---|
| FindByLogonTime | LastLogonTime | 指定時間內登入的帳戶 |
| FindByExpirationTime | AccountExpirationDate | 指定時間內過期的帳戶 |
| FindByPasswordSetTime | LastPasswordSetTime | 指定時間內設定密碼的帳戶 |
| FindByLockoutTime | AccountLockoutTime | 指定時間內被鎖定的帳戶 |
| FindByBadPasswordAttempt | LastBadPasswordAttempt | 指定時間內密碼錯誤嘗試的帳戶 |
| 無對應方法 | BadLogonCount | 指定次數內登入失敗的帳戶 |
唯讀屬性無法直接在 QBE 物件設定,但可透過 AdvancedSearchFilter 屬性結合查詢:
DateTime dt = DateTime.Today;
UserPrincipal user = new UserPrincipal(adPrincipalContext);
user.Enabled = true;
user.Name = "*";
user.AdvancedSearchFilter.LastBadPasswordAttempt(dt, MatchType.GreaterThanOrEquals);
PrincipalSearcher pS = new PrincipalSearcher(user);
PrincipalSearchResult<Principal> results = pS.FindAll();
foreach (UserPrincipal result in results)
{
Console.WriteLine("name: {0}, {1}", result.Name, result.LastBadPasswordAttempt.Value);
}
PrincipalContext.ValidateCredentials 提供高效能、安全的憑證驗證。建立上下文後重複呼叫驗證不同使用者,連線可重用,執行緒安全。
PrincipalContext ldsContext = new PrincipalContext(
ContextType.ApplicationDirectory,
"sea-dc-02.fabrikam.com:50000",
"ou=ADAM Users,O=Microsoft,C=US");
Console.WriteLine(ldsContext.ValidateCredentials(
"user1@adam", "Password1",
ContextOptions.SimpleBind | ContextOptions.SecureSocketLayer));
AccountManagement 盡可能使用快速並發繫結 (Fast Concurrent Bind),專為高效能驗證設計,不建構安全性權杖,連線保持未繫結狀態,僅適用簡單繫結模式,必須搭配 SSL 確保安全。
開發者常需建立自訂結構描述擴充支援專屬中繼資料。AccountManagement 利用 .NET 面向物件設計與屬性型擴充中繼資料,輕鬆建立自訂安全主體類別:繼承 Principal 衍生類別,以適當屬性標記類別與屬性,即可讀寫自訂目錄屬性。
[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");
return (result != null) ? (string)result[0] : null;
}
set { this.ExtensionSet("msdn-subscriberID", value); }
}
}
關鍵屬性:
DirectoryObjectClass:指定 objectClass 目錄屬性值DirectoryRdnPrefix:指定相對辨別名稱 (RDN) 屬性前綴 (AD DS 固定為 CN,AD LDS 可自訂)DirectoryProperty:對應 LDAP 結構描述屬性名稱使用 ExtensionGet/ExtensionSet 存取底層屬性快取,支援多值屬性 (陣列形式)。此模型還能將目錄屬性強型別化,例如將 jpgPhoto 表示為 System.Drawing.Image 或 System.IO.Stream 而非預設 byte[]。
System.DirectoryServices.AccountManagement 是微軟目錄服務程式設計模型不可或缺的托管程式碼補充。它封裝最佳實踐,助您編寫安全、高效能的程式碼;支援常見 CRUD 與搜尋操作的強類型主體;具備擴展性,可在 AD DS 和 AD LDS 中完整互動自訂目錄物件。
作者簡介:Joe Kaplan 任職於 Accenture 內部 IT 組織,專長應用程式安全性、聯合身分管理與目錄服務程式設計,榮獲 Microsoft MVP。合著《The .NET Developer's Guide to Directory Services Programming》(Addison-Wesley, 2006)。Ethan Wilansky 為 Microsoft 目錄服務 MVP 與 EDS 企業架構師,領導 SharePoint 應用程式開發團隊並提供目錄服務解決方案諮詢。