使用C#快速获取Active Directory中的组成员列表

在Web应用程序中,我们希望显示某个组成员的用户的sam帐户列表。 在很多情况下,组可以有500个或更多成员,我们需要页面响应。

拥有约500名成员的团队需要7-8秒才能获得该组所有成员的sam帐户列表。 有更快的方法吗? 我知道Active Directory管理控制台会在一秒钟内完成它。

我尝试过几种方法:

1)

PrincipalContext pcRoot = new PrincipalContext(ContextType.Domain) GroupPrincipal grp = GroupPrincipal.FindByIdentity(pcRoot, "MyGroup"); List lst = grp.Members.Select(g => g.SamAccountName).ToList(); 

2)

 PrincipalContext pcRoot = new PrincipalContext(ContextType.Domain) GroupPrincipal grp = GroupPrincipal.FindByIdentity(pcRoot, "MyGroup"); PrincipalSearchResult lstMembers = grp.GetMembers(true); List lst = new List(); foreach (Principal member in lstMembers ) { if (member.StructuralObjectClass.Equals("user")) { lst.Add(member .SamAccountName); } } 

3)

 PrincipalContext pcRoot = new PrincipalContext(ContextType.Domain) GroupPrincipal grp = GroupPrincipal.FindByIdentity(pcRoot, "MyGroup"); System.DirectoryServices.DirectoryEntry de = (System.DirectoryServices.DirectoryEntry)grp.GetUnderlyingObject(); List lst = new List(); foreach (string sDN in de.Properties["member"]) { System.DirectoryServices.DirectoryEntry deMember = new System.DirectoryServices.DirectoryEntry("LDAP://" + sDN); lst.Add(deMember.Properties["samAccountName"].Value.ToString()); } 

使用各种Active Directory检索方法时,我的同事在查询时间方面遇到类似问题。 他最终将信息缓存在数据库中并每晚刷新,然后只是访问数据库。

考虑到用户帐户不会经常改变,这对他来说是一个可以接受的妥协。 根据您的使用情况,这可能是也可能是不可接受的。

这是使用ADSI的递归搜索(在嵌套组中搜索用户)。

 static void Main(string[] args) { /* Connection to Active Directory */ string sFromWhere = "LDAP://SRVENTR2:389/dc=societe,dc=fr"; DirectoryEntry deBase = new DirectoryEntry(sFromWhere, "societe\\administrateur", "test.2011"); /* To find all the users member of groups "Grp1" : * Set the base to the groups container DN; for example root DN (dc=societe,dc=fr) * Set the scope to subtree * Use the following filter : * (member:1.2.840.113556.1.4.1941:=CN=Grp1,OU=MonOu,DC=X) */ DirectorySearcher dsLookFor = new DirectorySearcher(deBase); dsLookFor.Filter = "(&(memberof:1.2.840.113556.1.4.1941:=CN=Grp1,OU=MonOu,DC=societe,DC=fr)(objectCategory=user))"; dsLookFor.SearchScope = SearchScope.Subtree; dsLookFor.PropertiesToLoad.Add("cn"); dsLookFor.PropertiesToLoad.Add("samAccountName"); SearchResultCollection srcUsers = dsLookFor.FindAll(); /* Just show each user */ foreach (SearchResult srcUser in srcUsers) { Console.WriteLine("{0}", srcUser.Path); Console.WriteLine("{0}", srcUser.Properties["samAccountName"][0]); } Console.ReadLine(); 

}


对于@Gabriel Luci评论:Microsoft文档

成员

memberOf属性是一个多值属性,包含用户是直接成员的组,主要组除外,该组由primaryGroupId表示。 组成员身份取决于从中检索此属性的域控制器(DC):

  • 在包含用户的域的DC处,用户的memberOf在该域中的组的成员资格方面是完整的; 但是,memberOf不包含用户在其他域中的域本地和全局组中的成员身份。

  • 在GC服务器上,用户的memberOf已完成所有通用组成员身份。 如果DC的两个条件都为真,则两个数据集都包含在memberOf中。

请注意,此属性在其成员属性中列出包含用户的组 – 它不包含嵌套前驱的递归列表。 例如,如果用户O是组C的成员,组B和组B嵌套在组A中,则用户O的memberOf属性将列出组C和组B,但不列出组A.

此属性未存储 – 它是计算的反向链接属性。

试试这个不确定它是否会更快但……

  PrincipalContext pcRoot = new PrincipalContext(ContextType.Domain) GroupPrincipal mygroup = new GroupPrincipal(pcRoot); // define the principal searcher, based on that example principal PrincipalSearcher ps = new PrincipalSearcher(mygroup); ps.QueryFilter = new GroupPrincipal(pcRoot) { SamAccountName = "Name of your group Case Sensitive" }; List users = new List(); // loop over all principals found by the searcher GroupPrincipal foundGroup = (GroupPrincipal)ps.FindOne(); foreach (UserPrincipal u in foundGroup.Members) { users.Add(u); } //OR List lst = foundGroup.Members.Select(g => g.SamAccountName).ToList();//this will only get the usernames not the user object or UserPrincipal 

您是否尝试过LDAP查询? 页面底部有一个C#示例,用于枚举组以获取成员。 MSDN BOL

 using System; using System.DirectoryServices; namespace ADAM_Examples { class EnumMembers { ///  /// Enumerate AD LDS groups and group members. ///  [STAThread] static void Main() { DirectoryEntry objADAM; // Binding object. DirectoryEntry objGroupEntry; // Group Results. DirectorySearcher objSearchADAM; // Search object. SearchResultCollection objSearchResults; // Results collection. string strPath; // Binding path. // Construct the binding string. strPath = "LDAP://localhost:389/OU=TestOU,O=Fabrikam,C=US"; Console.WriteLine("Bind to: {0}", strPath); Console.WriteLine("Enum: Groups and members."); // Get the AD LDS object. try { objADAM = new DirectoryEntry(strPath); objADAM.RefreshCache(); } catch (Exception e) { Console.WriteLine("Error: Bind failed."); Console.WriteLine(" {0}", e.Message); return; } // Get search object, specify filter and scope, // perform search. try { objSearchADAM = new DirectorySearcher(objADAM); objSearchADAM.Filter = "(&(objectClass=group))"; objSearchADAM.SearchScope = SearchScope.Subtree; objSearchResults = objSearchADAM.FindAll(); } catch (Exception e) { Console.WriteLine("Error: Search failed."); Console.WriteLine(" {0}", e.Message); return; } // Enumerate groups and members. try { if (objSearchResults.Count != 0) { foreach(SearchResult objResult in objSearchResults) { objGroupEntry = objResult.GetDirectoryEntry(); Console.WriteLine("Group {0}", objGroupEntry.Name); foreach(object objMember in objGroupEntry.Properties["member"]) { Console.WriteLine(" Member: {0}", objMember.ToString()); } } } else { Console.WriteLine("Results: No groups found."); } } catch (Exception e) { Console.WriteLine("Error: Enumerate failed."); Console.WriteLine(" {0}", e.Message); return; } Console.WriteLine("Success: Enumeration complete."); return; } } 

}

与您的第一个选项类似,我从列表中创建了一个哈希集。 组越大,validation成员资格所需的时间越长。 但是,对于成功和不成功的成员资格查询,它是一致的。 如果帐户不是成员,则迭代一个大的组有时会花费3倍,而这种方法每次都是相同的。

 using(PrincipalContext ctx = new PrincipalContext(ContextType.Domain)) using(GroupPrincipal group = GroupPrincipal.FindByIdentity(ctx, IdentityType.SamAccountName, "groupName")) { List members = group.GetMembers(true).Select(g => g.SamAccountName).ToList(); HashSet hashset = new HashSet(members, StringComparer.OrdinalIgnoreCase); if(hashset.Contains(someUser) return true; } 

缓存组成员资格

Active Directory中的组成员身份不应经常更改。 因此,请考虑缓存组成员身份以更快地进行查找。 然后每小时或对您的环境最有意义的任何内容更新缓存的组成员身份。 这将极大地提高性能并减少网络和域控制器的拥塞。

需要注意的是,如果重要/受限制的信息受到保护,则需要加强安全控制。 然后直接查询Active Directory是可行的方法,因为它可以确保您拥有最新的成员资格信息。

如果您想要速度,请不要使用System.DirectoryServices.AccountManagement命名空间( GroupPrincipalUserPrincipal等)。 它使编码更容易,但它是slloooowwww。

仅使用DirectorySearcherDirectoryEntry 。 (无论如何, AccountManagement命名空间只是一个包装器)

不久前我和别人讨论过这个问题。 你可以在这里阅读完整的聊天 ,但是在一个组中有4873个成员的情况下, AccountManagementGetMember()方法耗时200秒,使用DirectoryEntry只需要16秒。

但是,有一些警告:

  1. 不要查看memberOf属性(正如JPBlanc的回答所示)。 它不会找到Domain Local组的成员。 memberOf属性仅显示通用组,而全局组仅显示在同一域中。 域本地组不会显示在那里。
  2. 查看组的member属性一次只能为您提供1500个成员。 您必须批量检索1500个成员。
  3. 帐户可以将primaryGroupId设置为任何组,并被视为该组的一部分(但不会显示在该组的member属性中)。 这通常仅适用于Domain Users组。
  4. 如果域本地组具有来自外部域的用户,则它们将显示为外部安全主体对象,该对象保存外部域上实际帐户的SID。 需要完成一些额外的工作才能在外部域上查找帐户。

AccountManagement命名空间的GetMember()方法负责所有这些事情,但效率却不GetMember()

在帮助其他用户时,我确实整理了一个方法,该方法将涵盖上述前三个问题,但不包括#4。 这是这个答案中的最后一个代码块: https : //stackoverflow.com/a/49241443/1202807

更新:您提到最耗时的部分是循环遍历成员。 那是因为你对每个成员都有约束力,这是可以理解的。 您可以通过调用DirectoryEntry对象上的.RefreshCache()来减少它,只加载您需要的属性。 否则,当您第一次使用“ Properties ,它将获得具有值的每个属性,这会毫无理由地增加时间。

以下是我使用的示例。 我测试了一个拥有803个成员的组(在嵌套组中)并且发现.RefreshCache()行持续削减大约10秒,如果不是更多(约60秒没有,~45-50s)。

这种方法不会解释我上面提到的第3和第4点。 例如,它会默默地忽略外部安全主体。 但是,如果您只有一个没有信任的域名,则无需关心。

 private static List GetGroupMemberList(DirectoryEntry group, bool recurse = false) { var members = new List(); group.RefreshCache(new[] { "member" }); while (true) { var memberDns = group.Properties["member"]; foreach (var member in memberDns) { var memberDe = new DirectoryEntry($"LDAP://{member}"); memberDe.RefreshCache(new[] { "objectClass", "sAMAccountName" }); if (recurse && memberDe.Properties["objectClass"].Contains("group")) { members.AddRange(GetGroupMemberList(memberDe, true)); } else { var username = memberDe.Properties["sAMAccountName"]?.Value?.ToString(); if (!string.IsNullOrEmpty(username)) { //It will be null if this is a Foreign Security Principal members.Add(username); } } } if (memberDns.Count == 0) break; try { group.RefreshCache(new[] {$"member;range={members.Count}-*"}); } catch (COMException e) { if (e.ErrorCode == unchecked((int) 0x80072020)) { //no more results break; } throw; } } return members; }