作者:Duncan Mackenzie
Microsoft Developer Network
2002 年 11 月 1 日
摘要: Duncan Mackenzie 說明如何使用 System.DirectoryServices 命名空間來搜尋 Microsoft Active Directory 中的資訊。(列印共 18 頁)
適用於:
Microsoft® .NET
Microsoft Windows®
Microsoft Active Directory®
下載此文章內的程式碼 (英文)
(請注意:範例程式碼中的註解均為英文,此文章中所顯示的中文化註解,僅供參考)
內容
簡介
從 ADSI 移到 System.DirectoryServices
建立連線
連結目錄路徑
驗證目錄
執行搜尋
其他選項
建立快速搜尋示範
將搜尋移到背景執行緒
連結 DirectoryServices 物件的資料
總結
簡介
Active Directory 是在組織中複製的一個特殊用途資料庫,容易延伸,非常適合用來儲存使用者資訊、網路組態和整個公司都必須能夠存取的其他資料。它是最強大的作業系統功能之一,如果您組織中有它,在建置應用程式時,它就可以有所發揮。
在本篇文章中,我將細述如何使用 System.DirectoryServices 命名空間來連接到 Active Directory,搜尋物件及顯示搜尋結果。當然,System.DirectoryServices 不只使用於 Active Directory—它也可以使用於數個不同的服務,包括廣泛採用的 LDAP 通訊協定—但在範例中,我會把焦點放在 Active Directory。
不過,我不會深入介紹 Active Directory。有關 Active Directory,請參閱以下一些相關的參考文章:
從 ADSI 移到 System.DirectoryServices
不熟悉 Active Directory 程式設計的人,對於 MSDN 及其他資源中的所有參考資料可能會感到困惑。ADSI (即 Active Directory Service Interfaces 的縮寫) 是一個 COM 程式庫,可讓您與非 .NET 語言的 Active Directory 互動。它是從 Microsoft® Visual Basic® 6.0、Visual Basic for Applications (VBA) 和指令碼語言存取目錄資訊最常見的方法。就其本身而論,關於 Active Directory 的許多範例和新聞群組公告將以 ADSI 為焦點。
還好,ADSI 使用的許多概念都可以輕易轉換成在 System.DirectoryServices 中使用—如同 ADSI 範例中顯示的任何 ADSI 路徑或屬性名稱。也可以在 COM 和 .NET 程式庫之間混合程式碼。System.DirectoryServices 命名空間內的許多方法及屬性均接受原始 ADSI 物件作為參數。當然,即使您從未使用過 ADSI,System.DirectoryServices 也容易學習及使用。
建立連線
在 System.DirectoryServices 命名空間內有兩個主要類別:DirectoryEntry 及 DirectorySearcher。(還有一些其他類別存在,但這些是您需要先使用的類別)。搜尋是使用適當命名的 DirectorySearcher 類別來執行。其使用方式是不需設定任何選項或是提供一個 DirectoryEntry 根物件即可。
在建立 DirectoryEntry 物件作為根物件使用時,您需要指定路徑來描述您要連接的服務。您也要指定安全性憑證。建立此單一物件會建立與您目錄的連線。您可以提供這個 DirectoryEntry 物件作為搜尋的根物件,來使用此連線進行查詢。另外,如果您未指定根物件,DirectorySearcher 會自動連結到目前的網域,並使用 Microsoft® Windows® 憑證進行驗證。
連結目錄路徑
若要連接 Active Directory,您可以使用 Global Catalog (GC://) 語法指定路徑,或使用標準 LDAP 路徑 (LDAP://)。使用的語法和路徑視網路環境而定。例如,在使用內部網路時,我決定使用 Global Catalog 語法,因為它可讓我搜尋整個企業網路 (整個 Active Directory 樹系)。我不包括伺服器名稱的路徑,而只指定 "GC://dc=home,dc=duncanmackenzie,dc=net" (假設我的網域叫作 home.duncanmackenzie.net),它讓 ADSI 連接到指定網域的通用類別伺服器。您也可以不指定任何路徑來連接。如果您使用 DirectorySearcher 物件而未提供任何根物件,它會自動使用目前網域進行搜尋。如需有關判斷網路正確路徑的詳細資訊,請閱讀這些參考資料:
驗證目錄
您一旦有了 LDAP 或 GC 路徑,接下來要關心的就是安全性憑證。DirectoryEntry 類別的建構函式可讓您指定使用者 ID 和密碼,或者,您可以在建立物件的執行個體之後設定使用者 ID 和密碼屬性。避免在程式碼中儲存實際的密碼/使用者 ID。而是向使用者擷取此資訊,更好的作法是使用 [整合式驗證] 。
Dim rootEntry _
As New DirectoryEntry("GC://dc=home,dc=duncanmackenzie,dc=net", _
userID.Text, _
password.Text)
注意 若要使這個程式碼有效,您需要在專案中參照 System.DirectoryServices,並將 Imports System.DirectoryServices (Visual Basic .NET) 或 using System.DirectoryServices; (C#) 這一行加入原始程式檔的最上面。
另外,如果您完全未指定使用者 ID 和密碼,System.DirectoryServices 會試著使用 [Windows 整合式驗證] 來連接。一般而言,我比較喜歡使用 [整合式驗證] 選項,至少在 Microsoft® Windows Forms 應用程式中我會這麼做,因為每一個使用者結束時對 Active Directory 只剩下他們現有的權限組。使用寫在程式內的使用者 ID 和密碼會導致我的應用程式賦予使用者對 Active Directory 有更多的存取權,超過他們應該擁有的權限,因而造成安全性風險。
執行搜尋
您一旦建立了初始 (根) DirectoryEntry,就可以開始使用 DirectorySearcher 類別執行查詢。執行簡單查詢只需要幾個步驟:
- 建立 DirectorySearcher 的執行個體,可選擇使用一個 DirectoryEntry 根物件。
- 將搜尋篩選條件設成字串來描述搜尋準則。
- 在 PropertiesToLoad 集合中填入您要為每一個搜尋結果擷取的 Active Directory 屬性清單。
- 執行 FindAll 方法及擷取結果。
實際上您可以幾乎略過這些所有的步驟,以預設值來執行搜尋。如果您未提供 DirectoryEntry 根物件,DirectorySearcher 會連結目前的網域。如果您未提供搜尋篩選條件,預設值是擷取全部物件,如果您未指定任何 PropertiesToLoad 值,則會擷取所有屬性 (您有權限讀取的屬性)。這些預設值讓使用 DirectoryServices 類別變得很容易。不過,除非應用程式真得需要所有屬性和所有物件,否則,我建議您使用至少一個篩選條件來限制搜尋,以及限制載入的屬性集。
Dim searcher As New DirectorySearcher(rootEntry)
searcher.PropertiesToLoad.Add("cn")
searcher.PropertiesToLoad.Add("mail")
'searcher.PropertiesToLoad.AddRange(New String() {"cn", "mail"})
'也有效,並可減少一些程式碼
searcher.Filter = "(&(anr=duncan)(objectCategory=person))"
Dim results As SearchResultCollection
results = searcher.FindAll()
其他選項
有許多其他的搜尋選項不是必要的,但對查詢的行為有很大的影響。以下是這些選項的其中一部分及對其用法的簡短說明:
- CacheResults 決定搜尋結果是否儲存在本機用戶端電腦上。如果您使用此設定,就能夠來回地巡覽結果集;否則,您就會以 foward-only 模式來巡覽結果。
- ClientTimeout、ServerTimeLimit、ServerPageTimeLimit 都是逾時值,可避免搜尋執行太久。第一個值 ClientTimeout 可控制用戶端等待的時間。其他兩個時間限制由伺服器規定。我建議至少設定 ClientTimeout,以避免應用程式無限期等待下去。不過,有一點要注意,如果伺服器端的其中一個時間限制到了,則會傳回到這個時間點為止所擷取的項目。如果先超過用戶端時間限制,則不傳回任何東西。請記住,伺服器本身有時間限制,可由系統管理員加以設定,因此可能在您指定的時間限制之前就會逾時 (並傳回不完整的結果)。
- PageSize 決定一次應傳回的項目數。若未設定 PageSize 屬性,或將它設定為 0,即表示應一次傳回所有結果。不過,使用分頁會使應用程式的回應速度更快。請記住,伺服器將決定在搜尋中傳回的物件數目上限,確保使用者不會使系統負荷過重。因此建議使用分頁,尤其當您可能有大型結果集的時候。在接近本篇文章結尾處的示範檔中,我會告訴您如何使用分頁及多執行緒來產生不讓使用者久等的搜尋表單。
使用結果
您一旦執行搜尋,就會傳回 SearchResultCollection 類別的執行個體,它可讓您列舉結果。不論您是否使用分頁,您都可以用相同方式存取搜尋結果。如果結果的下一頁還沒有產生,就會阻止列舉直到結果就緒為止。
Dim result As SearchResult
For Each result In results
MessageBox.Show(result.Properties("cn")(0))
Next
建立快速搜尋示範
為了將所有步驟全放在一起,我建立了一個簡單的應用程式來執行 Active Directory 的搜尋功能。此範例已包括在本篇文章的下載版本中 (請按一下本篇文章最上面的連結),您也可以按照它來建立自己的範例。
為了適當設定搜尋,包括安全性選項在內,我在一開始使用的空白 Windows Form 上設定了一些控制項。我在結尾處設定四個 TextBox 控制項 (root path、search string、user ID 和 password)、一個可讓我在 integrated authentication 和使用 user ID 與 password 之間切換的 CheckBox、一個執行搜尋的按鈕以及一個保存結果的 ListBox。我花了一點時間排列控制項及設定錨點屬性,以產生比較賞心悅目的外觀以及可控制大小調整的介面,當然您可以自己選擇要不要這麼做。

[圖 1] 此完成表單可讓您執行簡單搜尋。
現在,我的按鈕的按鍵事件中,就會使用這四個 TextBox 及 CheckBox 中所輸入的值,並執行我的搜尋。
Dim rootEntry _
As New DirectoryEntry(rootPath.Text)
If Not integratedAuth.Checked Then
rootEntry.Username = userID.Text
rootEntry.Password = password.Text
End If
Dim searcher As New DirectorySearcher(rootEntry)
searcher.PropertiesToLoad.Add("cn")
searcher.PropertiesToLoad.Add("telephoneNumber")
'searcher.PropertiesToLoad.AddRange(New String() {"cn", "mail"})
'也有效,並可減少一些程式碼
searcher.PageSize = 5
searcher.ServerTimeLimit = New TimeSpan(0, 0, 30)
searcher.ClientTimeout = New TimeSpan(0, 10, 0)
searcher.Filter = searchString.Text
Dim queryResults As SearchResultCollection
queryResults = searcher.FindAll()
一旦有了結果,我就將每一個結果加入 ListBox。
Dim result As SearchResult
For Each result In queryResults
result.Items.Add(result.Properties("cn")(0))
Next
注意 知道要使用哪些屬性,以及如何擷取它們,是更複雜的 System.DirectoryServices 概念之一。Active Directory 屬性名稱的理想來源 (至少當您在處理使用者和帳戶時) 是 SDK reference (英文),它對映屬性名稱至 Windows 的 User and Groups 嵌入式管理單元中的資訊。還有一點也很重要,許多屬性可能是多重值,因此一個屬性值若以陣列顯示,它其中可能包含其他許多個值。在範例中,我存取屬性值陣列的第一個成員來擷取 cn 屬性。因為我剛好知道 cn 屬性只包含單一值,所以才能這麼做。在使用各種不同屬性時,您可能需要搜尋每一個屬性,以判斷它是單值或多值。MSDN 上的 [Active Directory 架構] 參考資料是適合此用途的理想資源,它提供每一個屬性的詳細資訊,包括其資料型別,以及它們是單值或多值。
完成後的上述範例顯示如何對 Active Directory 執行搜尋,但這裡我想談談兩個議題。首先,此搜尋是對與我的使用者介面 (Form) 相同的執行緒執行搜尋,這表示每當我的程式碼等待 DirectoryServices 物件回應時,我的使用者介面會變成沒有回應。第二個議題是,我自行將我的擷取結果加入 ListBox 中,但許多程式設計師更熟悉以資料連結連接一組資料與使用者介面。在本篇文章的剩餘內容,我會告訴您如何使用背景執行緒來提供一個快速回應的 UI,以及您要如何在執行時期建立 DataTable 來提供連結功能。
將搜尋移到背景執行緒
每當我想要在背景執行緒執行工作時 (任何工作都可以,而不只是 DirectoryServices 工作),我最後都會遵循相同模式。首先,我會描述該模式,然後我會告訴您如何將它套用到 Directory Search 的特定工作中。
我建立類別來封裝背景工作。此類別包括:
- 用來設定工作的屬性。
- 無法使用它自己的任何參數的方法 (這就是為什麼我使用屬性的原因),它會在新的執行緒執行
- 引發一或多個事件,來傳達背景工作的進度 (和結束)。
注意 如果我使用來自 Windows Form 的這個模式,我必須避免背景工作所引發的事件更新了 Form 本身,並使用 Form 的 Invoke 方法,在兩個執行緒之間正確地進行封送處理。
要在稍早所建立的搜尋範例中套用此模式 (在下載版本中建立 "DS Background" 專案),需要加入新類別 BackgroundSearch。BackgroundSearch 有許多屬性,因此可適當設定搜尋,且它有 StartSearch 方法。在找到每一個結果時,就會引發 ResultFound (我的命名慣例似乎沒有什麼創意) 事件。完成整個搜尋時,會發生 SearchCompleted 事件。
Imports System
Imports System.DirectoryServices
Public Class BackgroundSearch
Dim m_FilterString As String
Dim m_PageSize As Integer
Dim m_RootPath As String
Dim m_PropertiesToLoad() As String
Dim m_IntegratedAuthentication As Boolean
Dim m_UserID As String
Dim m_Password As String
Public Property IntegratedAuthentication() As Boolean
Get
Return m_IntegratedAuthentication
End Get
Set(ByVal Value As Boolean)
m_IntegratedAuthentication = Value
End Set
End Property
Public Property UserID() As String
Get
Return m_UserID
End Get
Set(ByVal Value As String)
m_UserID = Value
End Set
End Property
Public Property Password() As String
Get
Return m_Password
End Get
Set(ByVal Value As String)
m_Password = Value
End Set
End Property
Public Property PropertiesToLoad() As String()
Get
Return m_PropertiesToLoad
End Get
Set(ByVal Value As String())
m_PropertiesToLoad = Value
End Set
End Property
Public Property FilterString() As String
Get
Return m_FilterString
End Get
Set(ByVal Value As String)
m_FilterString = Value
End Set
End Property
Public Property PageSize() As Integer
Get
Return m_PageSize
End Get
Set(ByVal Value As Integer)
m_PageSize = Value
End Set
End Property
Public Property RootPath() As String
Get
Return m_RootPath
End Get
Set(ByVal Value As String)
m_RootPath = Value
End Set
End Property
Public Event ResultFound(ByVal result As SearchResult)
Public Event SearchCompleted(ByVal entriesFound As Integer)
Public Sub StartSearch()
Dim rootEntry _
As New DirectoryEntry(RootPath)
If Not IntegratedAuthentication Then
rootEntry.Username = UserID
rootEntry.Password = Password
End If
Dim searcher As New DirectorySearcher(rootEntry)
searcher.PropertiesToLoad.AddRange(PropertiesToLoad)
searcher.PageSize = PageSize
searcher.ServerTimeLimit = New TimeSpan(0, 10, 0)
searcher.Filter = FilterString
Dim queryResults As SearchResultCollection
queryResults = searcher.FindAll()
Dim result As SearchResult
Dim resultCount As Integer = 0
For Each result In queryResults
RaiseEvent ResultFound(result)
resultCount += 1
Next
RaiseEvent SearchCompleted(resultCount)
End Sub
End Class
正如先前提到的,現在放入這些事件的事件處理常式中的任何程式碼將在背景執行緒上執行,而不在與 Form 本身相同的執行緒上執行。如果您想要修改自己的 Form (或您表單上的控制項),您必須對該呼叫進行封送處理,使它回到 Form 的執行緒上。我的範例處理此問題的方式是提供一個額外的程序來執行實際插入至 ListBox,並使用 Form 的 Invoke 方法,從事件處理常式中呼叫該程序。
Private Sub startSearch_Click( _
ByVal sender As Object, _
ByVal e As EventArgs) Handles startSearch.Click
With bkg
.RootPath = rootPath.Text
.FilterString = searchString.Text
If Not integratedAuth.Checked Then
.UserID = userID.Text
.Password = password.Text
End If
.PageSize = 5
.PropertiesToLoad = _
New String() {"cn", "mail", "telephoneNumber"}
Dim search As New _
Threading.Thread(AddressOf .StartSearch)
search.Start()
End With
End Sub
Private Sub bkg_ResultFound( _
ByVal result As SearchResult) _
Handles bkg.ResultFound
If result.Properties.Contains("mail") Then
Dim emailAddress As String
emailAddress = CStr(result.Properties("mail")(0))
Dim display As New displayResult _
(AddressOf AddTextToListBox)
Me.Invoke(display, New Object() {emailAddress})
End If
End Sub
Private Delegate Sub displayResult(ByVal textEntry As String)
Private Sub AddTextToListBox(ByVal textEntry As String)
results.Items.Add(textEntry)
End Sub
Private Sub bkg_SearchCompleted( _
ByVal entriesFound As Integer) _
Handles bkg.SearchCompleted
MessageBox.Show( _
String.Format("{0} Entries Found", entriesFound))
End Sub
如果您選擇在背景執行緒上執行搜尋,您可以試試分頁大小,因為這會影響使用者介面每一次更新的大小。
連結 DirectoryServices 物件的資料
目錄項目有動態屬性,這表示它們沒有預定的屬性集,而是在執行期間決定的屬性集合。這是 DirectoryServices 物件的理想模型,因為任何特定物件的可用屬性集並不固定。不過,當您嘗試與這些物件進行資料連結時,會發生問題。沒有固定屬性集,您就不能直接連結像 DataGrid 之類的控制項到 SearchResult 物件的集合。不過,遊戲尚未結束。您還是可以連結資料,只需要花一點工夫即可。
基本上,您必須將 DirectoryServices 的搜尋結果包裝至具有靜態屬性的其他某個物件,然後連結至這個新物件。基於我自己的用途,我有兩個選擇:我可以撰寫一個自訂類別來顯示自己想要的特定屬性,然後每次我在自己的 DirectoryServices 中加入或移除屬性時就可以變更該類別,或者,我可以動態地建立一個 DataTable,並使用它作為可連結的物件。DataTable 方式比較容易,而且同樣具有很好的作用,因此在這個狀況下我會選它。我的原始範例的第三個版本 ("DS DataBinding" 專案) 建立 DataTable 及連結其結果到 DataGrid。它包括在本篇文章的程式碼下載版本中。若要建立 DataTable 但無資料庫 (或 XML 檔),請建立一個新的 DataTable 執行個體,並針對每一個屬性,在其中加入 DataColumns。
Dim myTable As New DataTable("Results")
Dim colName As String
For Each colName In searcher.PropertiesToLoad
myTable.Columns.Add(colName, GetType(System.String))
Next
如果您選擇在背景執行緒上執行搜尋,您可以試試分頁大小,因為這會影響使用者介面每一次更新的大小。
連結 DirectoryServices 物件的資料
目錄項目有動態屬性,這表示它們沒有預定的屬性集,而是在執行期間決定的屬性集合。這是 DirectoryServices 物件的理想模型,因為任何特定物件的可用屬性集並不固定。不過,當您嘗試與這些物件進行資料連結時,會發生問題。沒有固定屬性集,您就不能直接連結像 DataGrid 之類的控制項到 SearchResult 物件的集合。不過,遊戲尚未結束。您還是可以連結資料,只需要花一點工夫即可。
基本上,您必須將 DirectoryServices 的搜尋結果包裝至具有靜態屬性的其他某個物件,然後連結至這個新物件。基於我自己的用途,我有兩個選擇:我可以撰寫一個自訂類別來顯示自己想要的特定屬性,然後每次我在自己的 DirectoryServices 中加入或移除屬性時就可以變更該類別,或者,我可以動態地建立一個 DataTable,並使用它作為可連結的物件。DataTable 方式比較容易,而且同樣具有很好的作用,因此在這個狀況下我會選它。我的原始範例的第三個版本 ("DS DataBinding" 專案) 建立 DataTable 及連結其結果到 DataGrid。它包括在本篇文章的程式碼下載版本中。若要建立 DataTable 但無資料庫 (或 XML 檔),請建立一個新的 DataTable 執行個體,並針對每一個屬性,在其中加入 DataColumns。
Dim myTable As New DataTable("Results")
Dim colName As String
For Each colName In searcher.PropertiesToLoad
myTable.Columns.Add(colName, GetType(System.String))
Next
一旦您開始接收結果,只要填入 DataTable 就可以開始進行。
Dim result As SearchResult
For Each result In queryResults
Dim dr As DataRow = myTable.NewRow()
For Each colName In searcher.PropertiesToLoad
If result.Properties.Contains(colName) Then
dr(colName) = CStr(result.Properties(colName)(0))
Else
dr(colName) = ""
End If
Next
myTable.Rows.Add(dr)
Next
results.SetDataBinding(myTable.DefaultView, "")
總結
System.DirectoryServices 可讓您輕易連接到 Active Directory,並在應用程式中發揮其功能,開啟無限的可能性。本篇文章中的範例只詳述此技術的一項用法—搜尋使用者目錄—但 System.DirectoryServices 命名空間還提供許多其他的公用程式,包括網路管理、伺服器組態...等等。
關於作者的背景資料,請參閱 GotDotNet 上的
Duncan Mackenzie 簡介 (英文)。