65.9K
CodeProject 正在变化。 阅读更多。
Home

(另一个) Windows 角色提供程序 - 用于企业环境

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2014 年 2 月 21 日

Eclipse

8分钟阅读

viewsIcon

11570

downloadIcon

114

为 AD 和本地计算机实现自定义 RoleProvider,用于 Forms Authentication。

引言

在企业环境中,某些情况下的倾向可能看起来是矛盾的。每个人都知道 SSO(单点登录)。这是一个很棒的功能,一旦登录 - 即通过 Active Directory 基础结构进行身份验证,第三方(外部)应用程序就不需要再次进行身份验证了。另一方面 - 即使有 Kerberos 或 NTLM 在场 - 您可能仍然需要在企业内部进行表单身份验证。为什么?出于明显的安全原因:一台无人看管的计算机就相当于一个无需重新身份验证即可访问所有应用程序的门户。当然,可以实现多种方法来防止无人看管的 PC 被解锁,但有很多地方这些方法没有被使用。另一方面,在某些情况下,重新身份验证被视为一种电子签名。

无论哪种情况,我的 ASP.NET (MVC3) 应用程序都需要表单身份验证,当然还需要针对 Active Directory 进行授权。

对于授权部分,您有 ActiveDirectoryMembershipProvider,但对于基于角色的授权,我没有找到任何有用的东西。AspNetWindowsTokenRoleProvider 看似很有希望,但它无法与表单身份验证一起使用,或者至少无法在不进行修改的情况下使用。所以我花了大量时间搜索以便找到一个可用的自定义角色提供程序。我必须承认,确实有一些存在,但没有一个满足我的需求。大多数都是基于 LDAP 的,这本身并不坏,但如果 System.DirectoryServices 命名空间就在手边,我真的需要自己进行 LDAP 操作吗?但最大的问题是,我发现的所有内容(例如, 这个)都没有考虑到企业 AD 可能非常庞大的事实。真的非常庞大。我在一个拥有数万用户的森林中工作。一个用户可以属于数百个组 - 更糟的是:嵌套组。其中许多组对于具体的应用程序毫无意义 - 如果只需要几个,为什么还要收集所有这些呢?还有一个重要主题:一个企业可能(将会)有自己的组命名策略(这可能会随时间而改变)。所以,开发人员在应用程序中硬编码的组名不是一个好的选择。

那么,真正需要的是什么?

我需要的是一个角色提供程序,它能够处理并同时忽略大量嵌套的组成员关系,并为我提供一个将应用程序角色与域安全组进行映射的可能性。我必须承认我没有研究过所有商业解决方案,我决定自己编写。我也编写了自己的成员资格提供程序,但这并不是真正必要的,因此我不会在这篇文章中介绍它。不过,它包含在源代码包中。

设计考虑因素

我的成员资格提供程序和角色提供程序都不需要处理用户管理。这通过 ADUC 或其他工具完成;因此,许多覆盖相应基类方法的实现将抛出 NotImplementedException。我需要在 AD 和非 AD 环境中运行我的代码,所以我决定定义一个可选的范围参数:可以是计算机或域 - 而不影响其他用法。当然,正如我之前提到的,我需要能够将应用程序角色映射到安全组。而这个映射放在哪里比放在 web.config 中,与其余配置放在一起更好?所以我需要一个配置处理程序。当然,这也可以通过更动态的方式实现,例如使用数据库表 - 如果您需要,请随意实现它,并且欢迎您与我们分享。 微笑 | :)

让我们从最后一个开始。

在 web.config 中存储映射

实现配置处理程序并不难,但本可以更简单地实现。您决定使用 XML 结构,然后需要为类中的相应元素和属性创建对应项。我选择了以下结构

<RoleGroups>
    <RoleWindowsGroups>
      <clear />
      <add WindowsGroupName="group1" RoleName="role1" IdentityType="identity type"/>
      <add WindowsGroupName="group2" RoleName="role2" />
      <add WindowsGroupName="group3" RoleName="role3" />
    </RoleWindowsGroups>
</RoleGroups>

角色名称是在应用程序中传递给 AuthorizeAttribute 的名称,而组名称是根据范围的 Windows 或 Active Directory 安全组标识符。第三个属性定义了 group 属性中的标识符所代表的 标识类型。这是可选的,默认情况下是 SamAccountName。在这种情况下,其格式可以是 domain/group_name(在森林中有用)或简单地 group_name。所以这是我需要为其创建配置处理程序的结构。实现包含许多需要编写的类和方法才能使其正常工作(为什么不能从 xsd 生成所有这些东西?) - 您可以在 ConfigurationHandler.cs 文件中看到所有这些。如果您对此主题感兴趣,请考虑阅读 这篇文章,当然还有 MSDN。要使用该处理程序,您需要在 web.config/<configuration>/<configSections> 部分中引用它,如下所示

<section name="RoleGroups" 
type="WinntSecurityProviders.RoleWindowsGroupSection, WinntSecurityProviders" />

文件中还包含(我知道,不是最佳实践)一个辅助类,它将配置中的映射转换为强类型列表

public sealed class RoleConfigurationHelper
    {
        public class RoleMapping
        {
            public string RoleName { get; private set; }
            public string WindowsGroupName { get; private set; }
            public IdentityType IdentityType { get; private set; }
           
            public RoleMapping(string RoleName, string WindowsGroupName, string IdentityType)
            {
                this.RoleName = RoleName;
                this.WindowsGroupName = WindowsGroupName;
                this.IdentityType = 
                     (IdentityType)Enum.Parse(typeof(IdentityType), IdentityType);
            }
        }
 
        private static IList<RoleMapping> RoleGroupsCache = new List<RoleMapping>();
 
        public static IEnumerable<RoleMapping> GetRoleGroups()
        {
            if (RoleGroupsCache.Count == 0)
            {
                try
                {
                    var sections = WebConfigurationManager.OpenWebConfiguration("/");
                    foreach (ConfigurationSection section in sections.Sections)
                    {
                        if (section is RoleWindowsGroupSection)
                        {
                            IList<RoleMapping> RoleGroups = new List<RoleMapping>();
 
                            foreach (GroupConfigElement RoleGroup in 
                                    (section as RoleWindowsGroupSection).Groups)
                            {
                                RoleGroups.Add(new RoleMapping
                                (RoleGroup.RoleName, RoleGroup.WindowsGroupName, 
                                RoleGroup.IdentityType));
                            }
 
                            RoleGroupsCache = RoleGroups;
                        }
                    }
                }
                catch (Exception ex)
                {
                    throw new ConfigurationErrorsException
                          ("Failed to load RoleWindowsGroupSection section", ex);
                }
            }
 
            return RoleGroupsCache;
        }
    }

映射经常被访问,因此我决定将其保留在内存中作为一个列表,而不是多次解析。

现在...

角色提供程序

我遇到的所有自定义提供程序都通过获取用户实体、解析其组隶属关系并将其返回到数组中来实现 GetRolesForUser 方法(以及所有其他方法)。首先,这无法处理嵌套组。其次,它将返回(并最终缓存)许多无用的组。所以我决定走相反的方向:即使有很多应用程序角色,它们的数量也将远少于用户可能所属的组。由于映射中定义了我感兴趣的所有组,我只需要处理这些组 - 从我的应用程序的角度来看,其他任何东西都没有用。这不是一项艰巨的任务,但我需要同时考虑范围。

正如我所提到的,我的提供程序的范围是计算机或域。当在 web.config 中添加提供程序时,会将其作为一个额外的属性传递

    <roleManager cacheRolesInCookie="true" 
    enabled="true" defaultProvider="WindowsRoleProvider">
      <providers>
        <clear />
        <add name="WindowsRoleProvider" 
        type="WinntSecurityProviders.WindowsRoleProvider, 
        WinntSecurityProviders" scope="Machine" />
      </providers>
    </roleManager>

构造函数接受属性作为键值集合。我实现了一些辅助方法,ToAuthenticationScope 就是其中之一,用于验证值并将值转换为两个定义的 enum 值。

public class WindowsRoleProvider : RoleProvider
{
 private SecurityProviderHelpers.AuthenticationScope scope;
 public override void Initialize
 (string name, System.Collections.Specialized.NameValueCollection config)
 {
  scope = SecurityProviderHelpers.ToAuthenticationScope(config["scope"]);
  base.Initialize(name, config);
 }

以下方法负责基于用户帐户名称和上述范围创建 UserPrincipal 对象。首先,它需要识别上下文,这实际上是范围的实际含义:要么是域本身,要么是计算机本身。由于用户以 DomainName\SamAccountName 格式进行身份验证(即使是本地计算机),因此必须将友好的域名称转换为 LDAP 路径。准备好上下文后,即可通过其帐户名称获取用户对象。请注意,在这种情况下,username 参数预计为上述格式。

 private UserPrincipal AsUserPrincipal(string username, out PrincipalContext context)
 {
  SecurityProviderHelpers.DomainUser dn = new SecurityProviderHelpers.DomainUser(username);
 
  if (scope == SecurityProviderHelpers.AuthenticationScope.Domain)
  {
   string domainName = SecurityProviderHelpers.FriendlyDomainToLdapDomain(dn.DomainMame);
   context = new PrincipalContext(ContextType.Domain, domainName);
  }
  else
  {
   context = new PrincipalContext(ContextType.Machine, dn.DomainMame);
  }
 
  return UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, dn.PrincipalName);
 }

此方法用于检查单个用户是否属于单个角色。正如我之前提到的 - 为了能够解决嵌套分组问题,我首先通过其映射的角色名称识别组,然后获取该组作为安全主体,之后才尝试深入匹配用户与组的成员。GroupPrincipal 具有 Members 属性。但此属性仅包含直接成员。幸运的是,有一个 GetMembers 方法重载,可以指示它执行递归搜索。而这正是我们需要的。

public override bool IsUserInRole(string username, string roleName)
 {
  try
  {
   PrincipalContext ctx;
   UserPrincipal user = AsUserPrincipal(username, out ctx);
 
   var KnownRoles = RoleConfigurationHelper.GetRoleGroups();
 
   if (!KnownRoles.Any(x => string.Equals
      (x.RoleName, roleName, StringComparison.OrdinalIgnoreCase)))
   {
    throw new ArgumentException(String.Format
    ("Role '{0}' is not mapped to any windows group", roleName), "RoleName");
   }
 
   var role = KnownRoles.First(x => string.Equals
              (x.RoleName, roleName, StringComparison.OrdinalIgnoreCase));
 
   GroupPrincipal group = GroupPrincipal.FindByIdentity
                          (ctx, role.IdentityType, role.WindowsGroupName);
 
   return group.GetMembers(true).Any(p => p.Sid == user.Sid);
  }
  catch
  {
   return false;
  }
 }

为了收集用户的角色,我采用了相同的策略:我获取所有角色,对于每个角色,我获取相应的组,如果找到用户是其成员(通过递归搜索),那么我将角色名称添加到结果中。

 public override string[] GetRolesForUser(string username)
 {
  try
  {
   PrincipalContext ctx;
   UserPrincipal user = AsUserPrincipal(username, out ctx);
   List<string> result = new List<string>();
 
   foreach (var role in RoleConfigurationHelper.GetRoleGroups())
   {
    GroupPrincipal group = GroupPrincipal.FindByIdentity
                           (ctx, role.IdentityType, role.WindowsGroupName);
    if (group.GetMembers(true).Any(p => p.Sid == user.Sid))
    {
     result.Add(role.RoleName);
    }
    group.Dispose();
   }
 
   user.Dispose();
 
   return result.ToArray();
  }
  catch
  {
   return new string[0];
  }
 }

正如您可能已经注意到的,这两个方法的代码并不直接依赖于范围,因为范围包含在 PrincipalContext 中,而框架代码为我们隐藏了差异。

让我们看下面的方法。在大多数情况下,它不是必需的,但由于它符合我的概念,非常优雅且简单,所以我实现了它。如果您对角色没有任何了解,这会是什么样子?您会枚举域中所有可能的安全组吗?

public override string[] GetAllRoles()
 {
  return (from role in RoleConfigurationHelper.GetRoleGroups() select role.RoleName).ToArray();
 }

为了获取角色中的用户,我不得不根据范围再次拆分逻辑。首先,我通过角色名称识别组,然后根据范围为其创建上下文。其余的与之前大致相同。

public override string[] GetUsersInRole(string roleName)
 {
  try
  {
   var roleGroup = RoleConfigurationHelper.GetRoleGroups().First
                   (x => string.Equals(x.RoleName, roleName, 
                   StringComparison.OrdinalIgnoreCase)).WindowsGroupName;
 
   SecurityProviderHelpers.DomainUser dn = new SecurityProviderHelpers.DomainUser(roleGroup);
   PrincipalContext ctx;
 
   if (scope == SecurityProviderHelpers.AuthenticationScope.Domain)
   {
    string domainName = SecurityProviderHelpers.FriendlyDomainToLdapDomain(dn.DomainMame);
    ctx = new PrincipalContext(ContextType.Domain, domainName);
   }
   else
   {
    ctx = new PrincipalContext(ContextType.Machine, dn.DomainMame);
   }
   GroupPrincipal group = new GroupPrincipal(ctx, dn.PrincipalName);
 
   return group.GetMembers(true).Select(x => x.SamAccountName).ToArray();
  }
  catch
  {
   return new string[0];
  }
 }
...
}

您可能会问嵌套组本身会发生什么 - 它们也会被返回吗?当然不会,在 MSDN 页面上,您可以阅读解释

当递归标志设置为 true 时,返回的主体集合不包含组对象;仅返回叶节点。

成员资格提供程序

它被创建以满足相同的需求,特别是通过将相同的参数传递给初始化程序,使其在计算机和域上下文中都可用。它包含了通过其中一个或另一个权威来验证用户凭据的代码。但正如前面提到的,大多数方法都没有实现,因为在企业 Active Directory 环境中,用户管理很少通过业务应用程序进行。

关注点

在我的情况下,启用了基于 cookie 的角色缓存,因为应用程序所支持的业务流程允许这样做。但添加一些其他方法(例如基于会话的(特别是与状态服务器一起))会很有趣 - 支持老化并增加允许在工作进程运行时更改映射的动态性。这将允许支持真正大型的企业级解决方案。

历史

  • 2014 年 2 月 21 日:初始版本
© . All rights reserved.