首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

.NET Framework 3.5 中管理目錄安全主體的全新體驗

     編輯:關於.NET

本文基於 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。

Microsoft 目錄服務程式設計架構圖
圖 1:Microsoft 目錄服務程式設計架構

開發者仍偏愛 ADSI 中用於管理安全主體 (使用者、群組) 的強類型介面。雖然 System.DirectoryServices 的通用類別能執行大部分任務,但操作並不直觀,許多細節模糊不清。.NET Framework 3.5 新增專為管理安全主體設計的 System.DirectoryServices.AccountManagement 命名空間 (本文簡稱 AccountManagement 命名空間),徹底解決此問題。

AccountManagement 三大設計目標

  • 簡化跨三大目錄平台的主體管理操作
  • 無論底層目錄為何,保持主體管理操作一致性
  • 提供可靠結果,免除開發者記憶每個注意事項與特殊情況

AccountManagement 命名空間採用提供者設計模式,在三大支援目錄平台上操作,確保各 Principal 類別成員行為一致,這是實現簡易性與一致性的關鍵。

System.DirectoryServices.AccountManagement 關鍵類別
圖 2:AccountManagement 命名空間關鍵類別

建立目錄上下文

使用 PrincipalContext 類別建立到目標目錄的連線,並指定操作憑證,類似 ActiveDirectory 命名空間中的 DirectoryContext。其建構函數提供多種重載,三個關鍵參數 ContextType、名稱、容器,比 DirectoryEntry 的路徑字串更具體、易懂。

ContextType 列舉指定目標目錄類型

  • Domain:Active Directory 域服務
  • ApplicationDirectory:AD LDS
  • Machine:本地 SAM 資料庫

名稱參數指定特定目錄 (伺服器、電腦或域名);容器參數指定目錄中目標位置。所有目錄支援 Windows Negotiate 驗證,Domain 和 ApplicationDirectory 預設使用帶簽章與密封的 Negotiate。

圖 3:建立上下文的三個範例
// 建立指向 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 即可在目錄間切換。

圖 5:建立 SAM 資料庫使用者帳戶
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();
圖 6:建立 AD LDS 使用者帳戶
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():取得使用者完整展開的安全性群組成員資格 (含巢狀群組)
圖 7:使用 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:依身分識別查詢

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();

PrincipalSearcher 與 QBE (Query By Example)

強大的 QBE 功能配合 PrincipalSearcher,可依條件查詢物件。建立範例物件、設定屬性作為查詢條件、執行搜尋:

圖 8:使用 PrincipalSearcher 查詢停用且名稱以 "user" 開頭的帳戶
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)。

專用 FindBy 方法簡化困難查詢

針對難以透過 QBE 查詢的唯讀屬性 (如 pwdLastSet 大整數),提供專用 FindBy 方法:

圖 9:查詢今日變更密碼的使用者
DateTime dt = DateTime.Today;
PrincipalSearchResult<Principal> results = UserPrincipal.FindByPasswordSetTime(
    adPrincipalContext, dt, MatchType.GreaterThanOrEquals);

foreach (Principal result in results)
{
    Console.WriteLine("name: {0}", result.Name);
}
圖 10:其他 FindBy 方法與對應唯讀屬性
方法名稱唯讀屬性說明
FindByLogonTimeLastLogonTime指定時間內登入的帳戶
FindByExpirationTimeAccountExpirationDate指定時間內過期的帳戶
FindByPasswordSetTimeLastPasswordSetTime指定時間內設定密碼的帳戶
FindByLockoutTimeAccountLockoutTime指定時間內被鎖定的帳戶
FindByBadPasswordAttemptLastBadPasswordAttempt指定時間內密碼錯誤嘗試的帳戶
無對應方法BadLogonCount指定次數內登入失敗的帳戶

唯讀屬性無法直接在 QBE 物件設定,但可透過 AdvancedSearchFilter 屬性結合查詢:

圖 11:使用 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 提供高效能、安全的憑證驗證。建立上下文後重複呼叫驗證不同使用者,連線可重用,執行緒安全。

圖 12:在 AD LDS 中驗證使用者
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 衍生類別,以適當屬性標記類別與屬性,即可讀寫自訂目錄屬性。

圖 13:自訂 MsdnUser 類別範例
[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 應用程式開發團隊並提供目錄服務解決方案諮詢。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved