.NET 中的用户组和角色管理与 Active Directory

Posted

技术标签:

【中文标题】.NET 中的用户组和角色管理与 Active Directory【英文标题】:User Group and Role Management in .NET with Active Directory 【发布时间】:2010-10-18 03:07:57 【问题描述】:

我目前正在研究为基于 .NET 的项目存储用户角色和权限的方法。其中一些项目是基于网络的,有些则不是。我目前正在努力寻找最佳方法,以跨项目类型以一致、可移植的方式实现我正在寻找的目标。

目前,我们希望利用 Active Directory 作为我们获取基本用户信息的单一联系点。因此,我们希望不必为每个应用程序的用户维护自定义数据库,因为它们已经存储在 Active Directory 中并在那里积极维护。此外,如果可能的话,我们不想编写自己的安全模型/代码,而是希望使用预先存在的东西,例如 Microsoft 提供的安全应用程序块。

某些项目只需要基本权限,例如读取、写入或无访问权限。其他项目需要更复杂的权限。这些应用程序的用户可能被授予访问某些区域的权限,但不能访问其他区域,并且他们的权限可以在每个区域中更改。应用程序的管理部分将控制和定义此访问权限,而不是 AD 工具。

目前,我们正在使用集成的 Windows 身份验证在我们的 Intranet 上执行身份验证。这对于查找基本用户信息非常有效,而且我已经看到可以扩展 ASP.NET 以提供 Active Directory 角色提供程序,因此我可以找出用户所属的任何安全组。但是,在我看来,这种方法的缺点是所有内容都存储在 Active Directory 中,如果事情变得太大,可能会导致维护混乱。

同样,我还听说过 Active Directory 轻量级目录服务,它似乎可以扩展我们的架构并仅添加应用程序特定的属性和组。问题是,我找不到任何关于如何完成或如何工作的信息。有一些 MSDN 文章描述了如何与这个实例对话以及如何创建一个新实例,但似乎没有任何东西可以回答我的问题。

我的问题是:根据您的经验,我是否走在正确的轨道上?我希望仅使用 Active Directory 即可实现,还是必须使用其他工具?


我研究过的其他方法:

使用多个 web.config 文件 [***] 创建自定义安全模型和数据库以跨应用程序管理用户

【问题讨论】:

我已按要求添加了示例代码 【参考方案1】:

使用 AD 进行身份验证是个好主意,因为无论如何您都需要在其中添加所有人,而对于 Intranet 用户,则无需额外登录。

您是正确的,ASP.NET 允许您使用允许您针对 AD 进行身份验证的提供程序,尽管没有任何内容可以为您提供组成员资格支持(尽管如果您愿意,实现起来非常简单,我可以提供样品)。

这里真正的问题是,如果您想使用 AD 组来定义每个应用程序内的权限,是吗?

如果是这样,那么您可以选择为 ASP.NET 创建自己的 RoleProvider,WinForms 和 WPF 应用程序也可以通过 ApplicationServices 使用它。此 RoleProvider 可以将 AD 中的用户 ID 链接到每个应用程序的组/角色,您可以将其存储在您自己的自定义数据库中,这也允许每个应用程序允许管理这些角色,而无需这些管理员在 AD 中具有额外权限。

如果您愿意,您还可以覆盖并将应用角色与 AD 组组合,因此,如果它们位于 AD 中的某个全局“管理员”组中,则无论应用角色成员身份如何,他们都可以在应用中获得完全权限。相反,如果他们在 AD 中有一个组或属性说他们已被解雇,您可以忽略所有 App 角色成员资格并限制所有访问权限(因为 HR 可能不会从每个应用程序中删除他们,假设他们甚至知道他们全部!)。

按要求添加示例代码:

注意:基于此原创作品http://www.codeproject.com/Articles/28546/Active-Directory-Roles-Provider

对于您的 ActiveDirectoryMembershipProvider,您只需要实现 ValidateUser 方法,尽管您可以根据需要实现更多,新的 AccountManagement 命名空间使这变得微不足道:

// assumes: using System.DirectoryServices.AccountManagement;
public override bool ValidateUser( string username, string password )

  bool result = false;

  try
  
    using( var context = 
        new PrincipalContext( ContextType.Domain, "yourDomainName" ) )
    
      result = context.ValidateCredentials( username, password );
    
  
  catch( Exception ex )
  
    // TODO: log exception
  

  return result;

对于您的角色提供者来说,这需要更多的工作,我们在搜索 google 时发现了一些关键问题,例如您要排除的组、您要排除的用户等。

可能值得一篇完整的博客文章,但这应该可以帮助您入门,它是在 Session 变量中缓存查找,作为如何提高性能的示例(因为完整的缓存示例太长了)。

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.Diagnostics;
using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;
using System.Linq;
using System.Web;
using System.Web.Hosting;
using System.Web.Security;

namespace MyApp.Security

    public sealed class ActiveDirectoryRoleProvider : RoleProvider
    
        private const string AD_FILTER = "(&(objectCategory=group)(|(groupType=-2147483646)(groupType=-2147483644)(groupType=-2147483640)))";
        private const string AD_FIELD = "samAccountName";

        private string _activeDirectoryConnectionString;
        private string _domain;

        // Retrieve Group Mode
        // "Additive" indicates that only the groups specified in groupsToUse will be used
        // "Subtractive" indicates that all Active Directory groups will be used except those specified in groupsToIgnore
        // "Additive" is somewhat more secure, but requires more maintenance when groups change
        private bool _isAdditiveGroupMode;

        private List<string> _groupsToUse;
        private List<string> _groupsToIgnore;
        private List<string> _usersToIgnore;

        #region Ignore Lists

        // IMPORTANT - DEFAULT LIST OF ACTIVE DIRECTORY USERS TO "IGNORE"
        //             DO NOT REMOVE ANY OF THESE UNLESS YOU FULLY UNDERSTAND THE SECURITY IMPLICATIONS
        //             VERYIFY THAT ALL CRITICAL USERS ARE IGNORED DURING TESTING
        private String[] _DefaultUsersToIgnore = new String[]
        
            "Administrator", "TsInternetUser", "Guest", "krbtgt", "Replicate", "SERVICE", "SMSService"
        ;

        // IMPORTANT - DEFAULT LIST OF ACTIVE DIRECTORY DOMAIN GROUPS TO "IGNORE"
        //             PREVENTS ENUMERATION OF CRITICAL DOMAIN GROUP MEMBERSHIP
        //             DO NOT REMOVE ANY OF THESE UNLESS YOU FULLY UNDERSTAND THE SECURITY IMPLICATIONS
        //             VERIFY THAT ALL CRITICAL GROUPS ARE IGNORED DURING TESTING BY CALLING GetAllRoles MANUALLY
        private String[] _defaultGroupsToIgnore = new String[]
            
                "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", "Everyone", "LOCAL", "Authenticated Users"
            ;
        #endregion

        /// <summary>
        /// Initializes a new instance of the ADRoleProvider class.
        /// </summary>
        public ActiveDirectoryRoleProvider()
        
            _groupsToUse = new List<string>();
            _groupsToIgnore = new List<string>();
            _usersToIgnore = new List<string>();
        

        public override String ApplicationName  get; set; 

        /// <summary>
        /// Initialize ADRoleProvider with config values
        /// </summary>
        /// <param name="name"></param>
        /// <param name="config"></param>
        public override void Initialize( String name, NameValueCollection config )
        
            if ( config == null )
                throw new ArgumentNullException( "config" );

            if ( String.IsNullOrEmpty( name ) )
                name = "ADRoleProvider";

            if ( String.IsNullOrEmpty( config[ "description" ] ) )
            
                config.Remove( "description" );
                config.Add( "description", "Active Directory Role Provider" );
            

            // Initialize the abstract base class.
            base.Initialize( name, config );

            _domain = ReadConfig( config, "domain" );
            _isAdditiveGroupMode = ( ReadConfig( config, "groupMode" ) == "Additive" );
            _activeDirectoryConnectionString = ReadConfig( config, "connectionString" );

            DetermineApplicationName( config );
            PopulateLists( config );
        

        private string ReadConfig( NameValueCollection config, string key )
        
            if ( config.AllKeys.Any( k => k == key ) )
                return config[ key ];

            throw new ProviderException( "Configuration value required for key: " + key );
        

        private void DetermineApplicationName( NameValueCollection config )
        
            // Retrieve Application Name
            ApplicationName = config[ "applicationName" ];
            if ( String.IsNullOrEmpty( ApplicationName ) )
            
                try
                
                    string app =
                        HostingEnvironment.ApplicationVirtualPath ??
                        Process.GetCurrentProcess().MainModule.ModuleName.Split( '.' ).FirstOrDefault();

                    ApplicationName = app != "" ? app : "/";
                
                catch
                
                    ApplicationName = "/";
                
            

            if ( ApplicationName.Length > 256 )
                throw new ProviderException( "The application name is too long." );
        

        private void PopulateLists( NameValueCollection config )
        
            // If Additive group mode, populate GroupsToUse with specified AD groups
            if ( _isAdditiveGroupMode && !String.IsNullOrEmpty( config[ "groupsToUse" ] ) )
                _groupsToUse.AddRange(
                    config[ "groupsToUse" ].Split( ',' ).Select( group => group.Trim() )
                );

            // Populate GroupsToIgnore List<string> with AD groups that should be ignored for roles purposes
            _groupsToIgnore.AddRange(
                _defaultGroupsToIgnore.Select( group => group.Trim() )
            );

            _groupsToIgnore.AddRange(
                ( config[ "groupsToIgnore" ] ?? "" ).Split( ',' ).Select( group => group.Trim() )
            );

            // Populate UsersToIgnore ArrayList with AD users that should be ignored for roles purposes
            string usersToIgnore = config[ "usersToIgnore" ] ?? "";
            _usersToIgnore.AddRange(
                _DefaultUsersToIgnore
                    .Select( value => value.Trim() )
                    .Union(
                        usersToIgnore
                            .Split( new[]  "," , StringSplitOptions.RemoveEmptyEntries )
                            .Select( value => value.Trim() )
                    )
            );
        

        private void RecurseGroup( PrincipalContext context, string group, List<string> groups )
        
            var principal = GroupPrincipal.FindByIdentity( context, IdentityType.SamAccountName, group );

            if ( principal == null )
                return;

            List<string> res =
                principal
                    .GetGroups()
                    .ToList()
                    .Select( grp => grp.Name )
                    .ToList();

            groups.AddRange( res.Except( groups ) );
            foreach ( var item in res )
                RecurseGroup( context, item, groups );
        

        /// <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 )
        
            string sessionKey = "groupsForUser:" + username;

            if ( HttpContext.Current != null &&
                 HttpContext.Current.Session != null &&
                 HttpContext.Current.Session[ sessionKey ] != null
            )
                return ( (List<string>) ( HttpContext.Current.Session[ sessionKey ] ) ).ToArray();

            using ( PrincipalContext context = new PrincipalContext( ContextType.Domain, _domain ) )
            
                try
                
                    // add the users groups to the result
                    var groupList =
                        UserPrincipal
                            .FindByIdentity( context, IdentityType.SamAccountName, username )
                            .GetGroups()
                            .Select( group => group.Name )
                            .ToList();

                    // add each groups sub groups into the groupList
                    foreach ( var group in new List<string>( groupList ) )
                        RecurseGroup( context, group, groupList );

                    groupList = groupList.Except( _groupsToIgnore ).ToList();

                    if ( _isAdditiveGroupMode )
                        groupList = groupList.Join( _groupsToUse, r => r, g => g, ( r, g ) => r ).ToList();

                    if ( HttpContext.Current != null )
                        HttpContext.Current.Session[ sessionKey ] = groupList;

                    return groupList.ToArray();
                
                catch ( Exception ex )
                
                    // TODO: LogError( "Unable to query Active Directory.", ex );
                    return new[]  "" ;
                
            
        

        /// <summary>
        /// Retrieve listing of all users in a specified role.
        /// </summary>
        /// <param name="rolename">String array of users</param>
        /// <returns></returns>
        public override string[] GetUsersInRole( String rolename )
        
            if ( !RoleExists( rolename ) )
                throw new ProviderException( String.Format( "The role '0' was not found.", rolename ) );

            using ( PrincipalContext context = new PrincipalContext( ContextType.Domain, _domain ) )
            
                try
                
                    GroupPrincipal p = GroupPrincipal.FindByIdentity( context, IdentityType.SamAccountName, rolename );

                    return (

                        from user in p.GetMembers( true )
                        where !_usersToIgnore.Contains( user.SamAccountName )
                        select user.SamAccountName

                    ).ToArray();
                
                catch ( Exception ex )
                
                    // TODO: LogError( "Unable to query Active Directory.", ex );
                    return new[]  "" ;
                
            
        

        /// <summary>
        /// Determine if a specified user is in a specified role.
        /// </summary>
        /// <param name="username"></param>
        /// <param name="rolename"></param>
        /// <returns>Boolean indicating membership</returns>
        public override bool IsUserInRole( string username, string rolename )
        
            return GetUsersInRole( rolename ).Any( user => user == username );
        

        /// <summary>
        /// Retrieve listing of all roles.
        /// </summary>
        /// <returns>String array of roles</returns>
        public override string[] GetAllRoles()
        
            string[] roles = ADSearch( _activeDirectoryConnectionString, AD_FILTER, AD_FIELD );

            return (

                from role in roles.Except( _groupsToIgnore )
                where !_isAdditiveGroupMode || _groupsToUse.Contains( role )
                select role

            ).ToArray();
        

        /// <summary>
        /// Determine if given role exists
        /// </summary>
        /// <param name="rolename">Role to check</param>
        /// <returns>Boolean indicating existence of role</returns>
        public override bool RoleExists( string rolename )
        
            return GetAllRoles().Any( role => role == rolename );
        

        /// <summary>
        /// Return sorted list of usernames like usernameToMatch in rolename
        /// </summary>
        /// <param name="rolename">Role to check</param>
        /// <param name="usernameToMatch">Partial username to check</param>
        /// <returns></returns>
        public override string[] FindUsersInRole( string rolename, string usernameToMatch )
        
            if ( !RoleExists( rolename ) )
                throw new ProviderException( String.Format( "The role '0' was not found.", rolename ) );

            return (
                from user in GetUsersInRole( rolename )
                where user.ToLower().Contains( usernameToMatch.ToLower() )
                select user

            ).ToArray();
        

        #region Non Supported Base Class Functions

        /// <summary>
        /// AddUsersToRoles not supported.  For security and management purposes, ADRoleProvider only supports read operations against Active Direcory. 
        /// </summary>
        public override void AddUsersToRoles( string[] usernames, string[] rolenames )
        
            throw new NotSupportedException( "Unable to add users to roles.  For security and management purposes, ADRoleProvider only supports read operations against Active Direcory." );
        

        /// <summary>
        /// CreateRole not supported.  For security and management purposes, ADRoleProvider only supports read operations against Active Direcory. 
        /// </summary>
        public override void CreateRole( string rolename )
        
            throw new NotSupportedException( "Unable to create new role.  For security and management purposes, ADRoleProvider only supports read operations against Active Direcory." );
        

        /// <summary>
        /// DeleteRole not supported.  For security and management purposes, ADRoleProvider only supports read operations against Active Direcory. 
        /// </summary>
        public override bool DeleteRole( string rolename, bool throwOnPopulatedRole )
        
            throw new NotSupportedException( "Unable to delete role.  For security and management purposes, ADRoleProvider only supports read operations against Active Direcory." );
        

        /// <summary>
        /// RemoveUsersFromRoles not supported.  For security and management purposes, ADRoleProvider only supports read operations against Active Direcory. 
        /// </summary>
        public override void RemoveUsersFromRoles( string[] usernames, string[] rolenames )
        
            throw new NotSupportedException( "Unable to remove users from roles.  For security and management purposes, ADRoleProvider only supports read operations against Active Direcory." );
        
        #endregion

        /// <summary>
        /// Performs an extremely constrained query against Active Directory.  Requests only a single value from
        /// AD based upon the filtering parameter to minimize performance hit from large queries.
        /// </summary>
        /// <param name="ConnectionString">Active Directory Connection String</param>
        /// <param name="filter">LDAP format search filter</param>
        /// <param name="field">AD field to return</param>
        /// <returns>String array containing values specified by 'field' parameter</returns>
        private String[] ADSearch( String ConnectionString, String filter, String field )
        
            DirectorySearcher searcher = new DirectorySearcher
            
                SearchRoot = new DirectoryEntry( ConnectionString ),
                Filter = filter,
                PageSize = 500
            ;
            searcher.PropertiesToLoad.Clear();
            searcher.PropertiesToLoad.Add( field );

            try
            
                using ( SearchResultCollection results = searcher.FindAll() )
                
                    List<string> r = new List<string>();
                    foreach ( SearchResult searchResult in results )
                    
                        var prop = searchResult.Properties[ field ];
                        for ( int index = 0; index < prop.Count; index++ )
                            r.Add( prop[ index ].ToString() );
                    

                    return r.Count > 0 ? r.ToArray() : new string[ 0 ];
                
            
            catch ( Exception ex )
            
                throw new ProviderException( "Unable to query Active Directory.", ex );
            
        
    

对此的示例配置子部分条目如下:

<roleManager enabled="true" defaultProvider="ActiveDirectory">
  <providers>
    <clear/>
    <add
        applicationName="MyApp" name="ActiveDirectory"
        type="MyApp.Security.ActiveDirectoryRoleProvider"
        domain="mydomain" groupMode="" connectionString="LDAP://myDirectoryServer.local/dc=mydomain,dc=local"
    />
  </providers>
</roleManager>

哇,代码太多了!

PS:上面角色提供者的核心部分是基于另一个人的工作,我手边没有链接,但我们通过谷歌找到了它,所以部分归功于那个人的原作。我们对其进行了大量修改以使用 LINQ 并摆脱了对数据库进行缓存的需求。

【讨论】:

很好的答案,你得到了我的尊重 @hometoast:我们实际上使用它在互联网上的 WPF 应用程序中针对私有内部 AD 域进行身份验证,请查看 MSDN 上的“应用程序服务”主题。它同样适用于 WinForms 应用程序。 惊人的答案!是否需要不支持写入方法(AddUsersToRole、CreateRole 等)?需要什么来支持这些操作?我希望我有更多的赞成票给你。 @Gabe:看看 System.DirectoryServices.AccountManagement 命名空间,有关于页面 (msdn.microsoft.com/en-us/library/bb384375.aspx) 和一个示例应用程序,显示如何创建用户等 (msdn.microsoft.com/en-us/library/bb384374.aspx)应该让你开始。 仅供参考,您的角色提供者所基于的代码的原始版本可以在 codeproject.com/KB/aspnet/active_directory_roles.aspx 找到。

以上是关于.NET 中的用户组和角色管理与 Active Directory的主要内容,如果未能解决你的问题,请参考以下文章

ASP.net Membership角色与权限管理

具有 Active Directory 和数据库角色的 Spring Security

Active Directory 角色:用户和读者之间的区别

访问Azure Active Directory用户和角色

具有自定义角色和 Active Directory 的 ASP MVC 5 Windows 身份验证

根据 ASP.NET Windows 身份验证的用户名与 Active Directory 中的登录名不匹配