Windows XP Windows 7 Windows 2003 Windows Vista Windows 綜合教學 Linux 系統教學
Windows 10 Windows 8 Windows 2008 Windows NT Windows Server 電腦軟體教學

使用 .NET Framework System.DirectoryServices 存取 Active Directory (下)

編輯:Windows Server 教學

接續前文:完成 DAL 動作方法與 ASP.NET 整合

承接上篇,我們已建立 DSUser 領域物件與 UserAdmin DAL 骨架。本文將實作核心業務邏輯方法,並示範 ASP.NET 管理頁面的關鍵技術——Windows 身分模擬,解決 ASP.NET 執行帳號權限不足無法寫入 AD 的問題。

一、 UserAdmin 核心動作方法實作

1. LoadUser - 依使用者名稱載入

public DSUser LoadUser(string username) {
    if (!connected) return null;
    try {
        // WinNT 提供者路徑格式: WinNT:///,user
        string userPath = loginPath.Replace(",computer", "") + "/" + username + ",user";
        DirectoryEntry userEntry = new DirectoryEntry(userPath, loginUsername, loginPassword, authType);
        object native = userEntry.NativeObject; // 強制繫結驗證存在
        return new DSUser(userEntry);
    } catch (Exception ex) {
        LogError("LoadUser failed: " + ex.Message);
        return null; // 找不到或錯誤
    }
}

2. AddUserToGroup - 將使用者加入群組

public bool AddUserToGroup(string groupName, DSUser dsUser) {
    if (!connected) return false;
    try {
        // 模擬高權限帳號執行 (見後續模擬章節)
        string groupPath = loginPath.Replace(",computer", "") + "/" + groupName + ",group";
        DirectoryEntry groupEntry = new DirectoryEntry(groupPath, loginUsername, loginPassword, authType);
        groupEntry.Invoke("Add", new object[] { dsUser.userDirEntry.Path });
        groupEntry.CommitChanges();
        return true;
    } catch (Exception ex) {
        LogError("AddUserToGroup failed: " + ex.Message);
        return false;
    }
}

3. GetGroups - 取得所有群組清單

public ArrayList GetGroups() {
    ArrayList groups = new ArrayList();
    if (!connected) return groups;
    try {
        DirectoryEntry root = new DirectoryEntry(loginPath, loginUsername, loginPassword, authType);
        foreach (DirectoryEntry child in root.Children) {
            if (child.SchemaClassName == "Group") {
                groups.Add(child.Name);
            }
        }
    } catch (Exception ex) { LogError("GetGroups failed: " + ex.Message); }
    return groups;
}

4. DeleteUser - 刪除使用者

public bool DeleteUser(DSUser dsUser) {
    if (!connected || dsUser.userDirEntry == null) return false;
    try {
        DirectoryEntry parent = dsUser.userDirEntry.Parent;
        parent.Children.Remove(dsUser.userDirEntry);
        return true;
    } catch (Exception ex) {
        LogError("DeleteUser failed: " + ex.Message);
        return false;
    }
}

5. SaveUser - 新增或更新使用者 (Upsert)

public DirectoryEntry SaveUser(DSUser dsUser) {
    if (!connected) return null;
    try {
        DirectoryEntry userEntry;
        // 嘗試載入現有使用者
        string userPath = loginPath.Replace(",computer", "") + "/" + dsUser.Username + ",user";
        try {
            userEntry = new DirectoryEntry(userPath, loginUsername, loginPassword, authType);
            object native = userEntry.NativeObject; // 存在則更新
        } catch {
            // 不存在則建立新物件
            DirectoryEntry parent = new DirectoryEntry(loginPath, loginUsername, loginPassword, authType);
            userEntry = parent.Children.Add(dsUser.Username, "user");
        }

        // 寫入屬性 (WinNT 使用 Invoke)
        userEntry.Invoke("Put", new object[] { "FullName", dsUser.FullName });
        userEntry.Invoke("Put", new object[] { "Description", dsUser.Description });
        // 密碼需透過 IADsUser::SetPassword (參考上篇範例)
        userEntry.CommitChanges();
        return userEntry;
    } catch (Exception ex) {
        LogError("SaveUser failed: " + ex.Message);
        return null;
    }
}

二、 ASP.NET 身分模擬:以高權限帳號執行 AD 寫入

ASP.NET 預設以 ASPNET (IIS 5) 或 Network Service (IIS 6+) 執行,無 AD 寫入權限。解決方案:在程式碼區塊中暫時模擬具備 Domain Admin 或委派權限的帳號。

1. P/Invoke 宣告

#region Impersonation Interop
public const int LOGON32_LOGON_INTERACTIVE = 2;
public const int LOGON32_PROVIDER_DEFAULT = 0;

[DllImport("advapi32.dll", CharSet = CharSet.Auto)]
public static extern int LogonUser(string lpszUserName, string lpszDomain,
    string lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);

[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int DuplicateToken(IntPtr hToken, int impersonationLevel, ref IntPtr hNewToken);
#endregion

private WindowsImpersonationContext impersonationContext;

2. 模擬輔助方法

private bool ImpersonateValidUser(string userName, string domain, string password) {
    IntPtr token = IntPtr.Zero, tokenDuplicate = IntPtr.Zero;
    if (LogonUser(userName, domain, password, LOGON32_LOGON_INTERACTIVE,
        LOGON32_PROVIDER_DEFAULT, ref token) != 0) {
        if (DuplicateToken(token, 2, ref tokenDuplicate) != 0) { // SecurityImpersonation = 2
            WindowsIdentity tempIdentity = new WindowsIdentity(tokenDuplicate);
            impersonationContext = tempIdentity.Impersonate();
            CloseHandle(token); CloseHandle(tokenDuplicate);
            return true;
        }
    }
    if (token != IntPtr.Zero) CloseHandle(token);
    if (tokenDuplicate != IntPtr.Zero) CloseHandle(tokenDuplicate);
    return false;
}

private void UndoImpersonation() {
    if (impersonationContext != null) {
        impersonationContext.Undo();
        impersonationContext = null;
    }
}

[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
private static extern bool CloseHandle(IntPtr handle);

3. 在頁面事件中使用模擬

protected void btnSave_Click(object sender, EventArgs e) {
    // 1. 取得管理帳號憑證 (建議從安全設定檔讀取,勿硬編碼)
    string adminUser = "adminUser"; string adminPwd = "P@ssw0rd"; string domain = "CORP";

    if (ImpersonateValidUser(adminUser, domain, adminPwd)) {
        try {
            // 2. 在模擬內容下執行 DAL 寫入操作
            UserAdmin dal = new UserAdmin(adminUser, adminPwd, AuthenticationTypes.Secure, domain);
            DSUser user = new DSUser(txtUsername.Text, txtPassword.Text, domain);
            user.FullName = txtFullName.Text;
            user.Description = txtDescription.Text;
            DirectoryEntry result = dal.SaveUser(user);
            if (result != null) lblMsg.Text = "儲存成功!";
            else lblMsg.Text = "失敗:" + dal.ErrorLog;
        } finally {
            // 3. 絕對要在 finally 區塊解除模擬
            UndoImpersonation();
        }
    } else {
        lblMsg.Text = "管理帳號模擬失敗,請檢查憑證與權限。";
    }
}

4. 必要的 machine.config 設定

為使模擬正常運作,需修改 %windir%\Microsoft.NET\Framework\\CONFIG\machine.config 中的 <processModel> 區段:

<processModel userName="SYSTEM" password="AutoGenerate" />

注意: 此設定影響整台伺服器所有 ASP.NET 應用程式,生產環境請謹慎評估安全性;替代方案為使用 <identity impersonate="true" userName="..." password="..." /> 在 web.config 設定應用程式層級模擬 (需 IIS 6+ 與特定權限)。

三、 完整 ASP.NET 管理頁面架構建議

  • 輸入區: 管理帳號/密碼 (或預設模擬帳號)、目標使用者帳號查詢框。
  • 顯示區: GridView/DetailsView 綁定 DSUser 屬性,支援編輯、刪除、新增。
  • 群組管理: 列出 GetGroups(),提供 CheckBoxList 讓使用者勾選成員資格,送出時呼叫 AddUserToGroup/移除。
  • 錯誤處理: 統一顯示 dal.ErrorLog,記錄詳細堆疊至伺服器日誌。

四、 進階擴充方向

  1. 群組完整 CRUD: 封裝 Create/Delete Group、群組巢狀管理,需透過 COM Interop 呼叫 IADsGroup 方法。
  2. LDAP 提供者優化: 改用 DirectorySearcher 執行分頁查詢、索引屬性篩選,大幅提升大型目錄效能。
  3. Schema 擴充: 動態新增自訂屬性 (如 EmployeeID、CostCenter),並更新 DAL 對應。
  4. 安全強化: 使用 System.DirectoryServices.Protocols (S.DS.P) 低階 LDAP 控制 TLS/SSL、SASL 驗證、分頁控制項。
  5. 單元測試: 引入 Microsoft.DirectoryServices.AccountManagement (System.DirectoryServices.AccountManagement, .NET 3.5+) 簡化測試模擬。

結語

透過 System.DirectoryServices 封裝 DAL、搭配 Windows 身分模擬,我們成功打造了一個可在 ASP.NET 中安全、高效操作 Active Directory 的完整方案。此架構將目錄操作複雜度隔離於 DAL,上層頁面專注於商務流程與 UI,符合關注點分離原則。未來遷移至 .NET Core / .NET 5+ 時,可改用 System.DirectoryServices.Protocols 或 Novell.Directory.Ldap.NETStandard 實現跨平台 LDAP 操作,核心 DAL 介面設計理念仍完全適用。

Copyright © Windows教程網 All Rights Reserved