Active Directory 角色提供程序
一个 Active Directory 角色提供程序

目录
引言
我编写此代码是为了填补 Microsoft 当前 ASP.NET 角色管理产品中的一个明显不足。(如果您不熟悉 ASP.NET 成员资格和角色管理,在继续阅读本文之前,您可能需要查看有关保护 ASP.NET 网站的当前框架文档。有关成员资格和角色的摘要页面应该能为您提供一个良好的开端。)
目前,如果您使用的是基于表单的身份验证以及 Microsoft 的 ActiveDirectoryMembershipProvider
,那么在角色提供程序的选项方面会受到一些限制。框架中没有内置任何像 ActiveDirectoryMembershipProvider
那样配置和维护相对简单的东西。您最好的解决方案是设置 AzMan。然而,它的学习曲线相当陡峭,并且在此之间存在很多相互冲突的信息,此外,它还需要在网站的 web.config
之外进行设置和配置。
这就是 ADRoleProvider
发挥作用的地方。简单来说,ADRoleProvider
是一个角色提供程序类,它允许您使用现有的 Active Directory 组作为 ASP.NET 角色,并努力提供与 ActiveDirectoryMembershipProvider
提供成员资格框架一样简单的框架。由于它继承自 Microsoft 的基类 RoleProvider
,因此它应该能够无缝地在您的站点中运行,并与所有标准 .NET 控件(LoginView 等)和 Web.config 授权设置配合使用。
请务必仔细阅读本文档,并理解其中提到的安全和性能注意事项。另外,请查看代码以了解其具体工作原理。
要求
为了使用这个 RoleProvider
类,只有两个要求:
- 您必须运行 ASP.NET 网站,并使用框架的 3.5 版本。这是因为代码使用了
System.DirectoryServices.AccountManagement
命名空间来枚举组的成员资格。 - 您的 IIS Web 服务器必须是您希望用于组/角色维护的域的成员。您可以稍微调整代码以使用非域服务器,我将在后续提供此选项的支持。
- 目前,在您的 ActiveDirectoryMembershipProvider 中应将 attributeMapUsername="sAMAccountName" 设置。这将在下面的技术摘要中进一步说明。
技术摘要
借鉴 Microsoft 已发布的 SqlRoleProvider
的源代码作为起点,我试图使此类尽可能地遵循“内置”角色提供程序的行为。唯一需要注意的重要区别是这是一个只读提供程序。由于管理应仅通过 Active Directory 进行,任何需要写入访问权限的角色操作都将引发 NotSupportedException
。
重要提示 - 该类中的所有 Active Directory 查询当前都针对 LDAP sAMAccountName 属性进行。因此,每次在 web.config 中引用组或用户名时,请务必使用 sAMAccountName。然而,ActiveDirectoryMembershipProvider
可以配置为使用 sAMAccountName 或 userPrincipalName。为避免任何混淆,最安全的做法是在 web.config 的成员资格配置部分设置 attributeMapUsername="sAMAccountName"。我目前计划对该代码进行的唯一关键更改是使用 UPN 的选项,以避免任何混淆。此更改将在大约一周内完成并经过测试。如果您正在一个实时环境中工作,其中用户习惯于使用 username@domain.com
而不是 username
登录,请在此之前暂缓使用。
源代码中会专门排除某些内置或常见的 Active Directory 组。例如,Exchange Domain Servers 绝不会用作角色,因此它在一个内部排除列表中。这些系统组无法查询,也永远不会出现在任何结果中。您也可以在 Web.config 中指定要忽略的组(请参见下面的配置)。
查询 Active Directory
此类完成的所有繁重工作都涉及查询 Active Directory,而查询 Active Directory 的方法之多,足以让你自己给自己找麻烦。下面是检索用户角色(即 AD 组)成员资格的完整代码。为了便于解释,SQL 缓存代码被省略了。
/// <summary>
/// Retrieve listing of all roles to which a specified user belongs.
/// </summary>
/// <param name="username"></param>
/// <returns>String array of roles</returns>
public override String[] GetRolesForUser(String username)
{
...
//Create an ArrayList to store our resultant list of groups.
ArrayList results = new ArrayList();
//PrincipalContext encapsulates the server or domain against which all
//operations are performed.
using (PrincipalContext context = new PrincipalContext(ContextType.Domain,
null, _DomainDN))
{
try
{
//Create a referance to the user account we are querying
//against.
UserPrincipal p = UserPrincipal.FindByIdentity(context,
IdentityType.SamAccountName, username);
//Get the user's security groups. This is necessary to
//return nested groups, but will NOT return distribution groups.
var groups = p.GetAuthorizationGroups();
foreach (GroupPrincipal group in groups)
{
if (!_GroupsToIgnore.Contains(group.SamAccountName))
{
if (_IsAdditiveGroupMode)
{
if (
_GroupsToUse.Contains(
group.SamAccountName))
{
results.Add(
group.SamAccountName);
}
}
else
{
results.Add(group.SamAccountName);
}
}
}
}
catch (Exception ex)
{
throw new ProviderException(
"Unable to query Active Directory.", ex);
}
}
...
return results.ToArray(typeof(String)) as String[];
}
不要让嵌套的 if
语句迷惑您;它们仅用于确保根据您的配置(如下所述)正确地忽略/使用相应的组。正如注释所述,针对 System.DirectoryServices.AccountManagement
进行编程实际上非常简单。
缓存到 SQL Server
查询 Active Directory 可能会非常慢。为了缓解这种情况,ADRoleProvider
提供了将 Active Directory 查询结果缓存到 Microsoft SQL Server 的选项。这似乎有些违反直觉,但查询 SQL Server 的缓存通常比执行 AD 查询更快。
缓存方式非常简单。我在使用更完全规范化的一系列表和最终确定的简化方法之间摇摆不定。数据被缓存到一个表中,如下所示。
- 一个 ID 字段,用于在其中一个存储过程中加速重复查找
- 项的应用程序名称,允许使用单个表处理多个应用程序
- 被缓存对象的类型(L=角色列表,U=用户成员资格,R=角色成员资格)
- 用户或角色名称
- 逗号分隔的成员资格列表
- 过期日期时间
CacheId ApplicationId CacheType CacheKey CacheValue ExpireDT
49 MyApp L AllRoles Customer Service,IT 8/18/2008 2:49:50 PM
50 MyApp U asmith IT 8/18/2008 2:49:50 PM
51 MyApp R IT dsmith,msmith,asmith,jsmith,ssmith,bsmith 8/18/2008 2:49:50 PM
在某些情况下,这种简化的缓存方法可能会带来一些小麻烦。例如:IT 的缓存结果设置为在 2:50 过期,asmith 的结果设置为在 3:50 过期,而 asmith 从 IT Active Directory 组中被移除。在这种情况下,可能会有一个小时的时间段,其中枚举 IT 显示 asmith 不是成员,但 asmith 仍然认为他是。这可以通过拥有一个组表、一个用户表和一个联接表来避免。然而,我认为增加的复杂性和开销不值得权衡,因为每次设置缓存项时,都需要验证所涉及的每个项的完整性。
包含在代码 zip 文件中的是一个 SQL 文件,它将创建实现缓存所需的表和存储过程。这已在 SQL Server 2005 Standard 和 Developer 版本以及 SQL Server 2008 Standard 中得到测试。
SQL 缓存性能
老实说,启用 SQL 缓存的影响比我预期的要大得多。我的测试环境只包含两个组和大约半打用户。在如此小的数据集下,我认为性能影响会很小。我连接的 SQL 实例位于与测试站点不同的服务器上。下面的代码基本上是我用来测试性能的代码,尽管我实际上将其设置为运行了几百次。
DateTime dtStart;
DateTime dtEnd;
TimeSpan executionTime;
string[] results;
dtStart = DateTime.Now;
results = Roles.GetAllRoles();
results = Roles.GetRolesForUser("dsoref");
results = Roles.GetUsersInRole("IT");
dtEnd = DateTime.Now;
executionTime = dtEnd - dtStart;
tests.InnerHtml += "Execution Time: " + executionTime.TotalMilliseconds + "<br /><br />";
在不启用缓存的情况下,这段代码需要 78 到 154 毫秒才能执行,平均值徘徊在 98 毫秒左右。
启用缓存后,首次请求需要 81 到 204 毫秒才能执行,平均值徘徊在 102 毫秒左右,因为数据需要缓存到 SQL 数据库。然而,在过期时间内的后续请求仅需要 14 到 16 毫秒。我猜 SQL 确实比 AD 快。
Using the Code
首先,您需要编译提供程序并将生成的 .dll 文件复制到您网站的 bin 文件夹中。您也可以将 .cs 文件复制到您的 App_Code 目录中,但这不太安全。诚然,您应该能够信任您所有的开发人员,但小心驶得万年船。
Web.Config
接下来,您需要将正确的配置设置输入到您的 web.config 中,如下所示。我将逐一介绍这些设置。
...
<connectionStrings>
...
<add name="ActiveDirCS"
connectionString="LDAP://DC=YourDomain,DC=com"/>
</connectionStrings>
...
<roleManager enabled="true" defaultProvider="ActiveDirRP">
<providers>
<clear/>
<add applicationName="MyApp"
name="ActiveDirRP"
type="DanielPS.Roles.ADRoleProvider"
activeDirectoryConnectionString="ActiveDirCS"
groupMode="Additive"
groupsToUse="IT, Customer Service"
groupsToIgnore="Senior Management"
usersToIgnore="asmith, ksose"
enableSqlCache="True"
sqlConnectionString="SQLCacheCS"
cacheTimeInMinutes="30" />
</providers>
</roleManager>
...
- Name 应像任何其他角色提供程序一样指定,以便在 web.config 中引用。
- Type 将指向我们的新角色提供程序类
DanielPS.Roles.ADRoleProvider
,如果您不喜欢命名空间,也可以是其他名称。 - ApplicationName 应与 membershipprovider 部分中使用的相同。
- activeDirectoryConnectionString 应为 LDAP 格式且无服务器,即
LDAP://DC=YourDomain,DC=com
。如果您指定了服务器,将会引发错误。 - 这里最重要的设置是 groupMode。它有两个选项:
- Additive(加法) - 除非在 groupsToUse 部分中指定,否则所有 Active Directory 组本质上都是不可见的且无用的。这是最安全的方法,您可以确保没有安全的 Active Directory 组暴露给网站,甚至在
GetAllRoles()
调用时也不会列出。所有您希望用作角色的组都必须在 groupsToUse 中指定。 - Subtractive(减法) - 除非在 groupsToIgnore 部分列出,否则所有 Active Directory 组都将作为角色公开。这安全性稍低,但在添加或删除组时需要更少的维护。如前所述,源代码中有一个常见 AD 组列表,无论是否在 groupsToIgnore 列表中,这些组都会被忽略。
- Additive(加法) - 除非在 groupsToUse 部分中指定,否则所有 Active Directory 组本质上都是不可见的且无用的。这是最安全的方法,您可以确保没有安全的 Active Directory 组暴露给网站,甚至在
- groupsToUse 是一个逗号分隔的组列表,这些组应被用作角色。仅当 groupMode 设置为 Additive 时才使用此选项。
- groupsToIgnore 是一个逗号分隔的组列表,这些组在角色方面应被忽略。它们不会出现在任何 Roles 函数的结果中。在上面的示例中,
Roles.GetRolesForUser()
永远不会包含 "Senior Management" 的结果,并且Roles.GetUsersInRole("Senior Management")
将引发ProviderException
。 - enableSqlCache 应设置为 True 以启用 SQL 缓存。我强烈推荐。
- sqlConnectionString 是使用 SQL 缓存时要使用的连接字符串的名称。
- cacheTimeInMinutes 是在启用 SQL 缓存时,项应缓存的时间长度(以分钟为单位)。
注意:如果 groupMode 为 additive,并且某个组同时在 groupsToUse 和 groupsToIgnore 中指定,则 groupsToIgnore 优先。
注意:如果一个组在 groupsToUse 中指定,但在 Active Directory 中不存在,则该组将被忽略。
源代码中排除的组和用户
以下组在源代码中被排除。即使在 groupsToUse 中指定它们,也无法使其生效。
Domain Guests, Domain Computers, Group Policy Creator Owners, Guests, Users, Domain Users, Pre-Windows 2000 Compatible Access, Exchange Domain Servers, Schema Admins, Enterprise Admins, Domain Admins, Cert Publishers, Backup Operators, Account Operators, Server Operators, Print Operators, Replicator, Domain Controllers, WINS Users, DnsAdmins, DnsUpdateProxy, DHCP Users, DHCP Administrators, Exchange Services, Exchange Enterprise Servers, Remote Desktop Users, Network Configuration Operators, Incoming Forest Trust Builders, Performance Monitor Users, Performance Log Users, Windows Authorization Access Group, Terminal Server License Servers, Distributed COM Users, Administrators, Everybody, RAS and IAS Servers, MTS Trusted Impersonators, MTS Impersonators
以下用户在源代码中被排除。它们不会出现在任何组的成员资格枚举中。
Administrator, TsInternetUser, Guest, krbtgt, Replicate, SERVICE, SMSService
未来的增强
- 添加配置选项,允许非域 IIS 服务器托管使用此代码的网站,而无需进行任何调整。
- 添加配置选项以使用 Common-Name 而不是 sAMAccountName。
历史
- 2008/8/12 - 初次发布
- 2008/8/20 - 更新以包含 SQL 缓存,重写部分内容以求清晰,提供更详细的解释,以及杂项更正
- 2008/8/20 - 重写组的成员资格枚举,以使用
System.DirectoryServices.AccountManagement
(仅限 .NET 3.5)并支持递归成员资格 - 2008/9/4 - 更新源代码
- 2008/9/30 - 文章重写