简体   繁体   English

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

[英]User Group and Role Management in .NET with Active Directory

I'm currently researching methods for storing user roles and permissions for .NET based projects. 我目前正在研究存储基于.NET的项目的用户角色和权限的方法。 Some of these projects are web based, some are not. 其中一些项目是基于网络的,有些则不是。 I'm currently struggling to find the best method to achieve what I'm looking for in a consistent, portable way across project types. 我目前正在努力寻找最佳方法来实现我正在寻找的跨项目类型的一致,可移植的方式。

Where I'm at, we're looking to leverage Active Directory as our single point of contact for basic user information. 在我所处的位置,我们希望利用Active Directory作为基本用户信息的单点联系人。 Because of this, we're looking to not have to maintain a custom database for each application's users since they are already stored in Active Directory and actively maintained there. 因此,我们希望不必为每个应用程序的用户维护自定义数据库,因为它们已存储在Active Directory中并在那里进行了主动维护。 Additionally, we don't want to write our own security model/code if possible and would like to use something pre-existing, like the security application blocks provided by Microsoft. 此外,我们不希望编写自己的安全模型/代码,并且希望使用预先存在的内容,例如Microsoft提供的安全应用程序块。

Some projects require only basic privileges, such as read, write, or no access. 某些项目仅需要基本权限,例如读取,写入或无访问权限。 Other projects require more complex permissions. 其他项目需要更复杂的权限。 Users of those applications might be granted access to some areas, but not others, and their permissions can change across each area. 这些应用程序的用户可能被授予某些区域的访问权限,但不能访问其他区域,并且他们的权限可以在每个区 An administration section of the app would control and define this access, not the AD tools. 应用程序的管理部分将控制和定义此访问权限, 而不是 AD工具。

Currently, we're using integrated Windows Authentication to perform authentication on our intranet. 目前,我们正在使用集成的Windows身份验证在我们的Intranet上执行身份验证。 This works well for finding out basic user information, and I've seen that ASP.NET can be extended to provide an Active Directory roles provider, so I can find out any security groups a user belongs to. 这非常适合查找基本用户信息,我已经看到ASP.NET可以扩展为提供Active Directory角色提供程序,因此我可以找到用户所属的任何安全组。 But, what seems like the downfall of this method to me is that everything is stored in Active Directory, which could lead to a mess to maintain if things grow too big. 但是,对我来说,这种方法的垮台似乎是所有内容都存储在Active Directory中,如果事情变得太大,可能会导致维护混乱。

Along this same line, I've also heard of Active Directory Lightweight Directory Services, which seems like it could extend our schema and add only application specific attributes and groups. 同样,我也听说过Active Directory轻量级目录服务,它似乎可以扩展我们的架构并只添加特定于应用程序的属性和组。 Problem is, I can't find anything on how this would be done or how this works. 问题是,我找不到任何关于如何做到这一点或如何工作的事情。 There are MSDN articles that describe how to talk to this instance and how to create a new instance, but nothing ever seems to answer my question. 有MSDN文章描述了如何与这个实例交谈以及如何创建一个新实例,但似乎没有什么能回答我的问题。

My question is: Based on your experience, am I going down the right track? 我的问题是:根据您的经验,我是否走上正轨? Is what I'm looking to do possible using just Active Directory, or do other tools have to be used? 我只想使用Active Directory做什么,还是必须使用其他工具?


Other methods I've looked into: 我研究过的其他方法:

  • Using multiple web.config files [ stackoverflow ] 使用多个web.config文件[ stackoverflow ]
  • Creating a custom security model and database to manage users across applications 创建自定义安全模型和数据库以跨应用程序管理用户

Using AD for your authentication is a great idea, since you need to add everyone there anyway, and for intranet users there's no need for an extra login. 使用AD进行身份验证是个好主意,因为无论如何都需要在那里添加所有人,而对于Intranet用户则不需要额外的登录。

You're correct that ASP.NET allows you to use a Provider which will allow you to authenticate against AD, although there's nothing included to give you group membership support (although it's quite trivial to implement if you want to, I can provide a sample). 你是正确的,ASP.NET允许你使用一个允许你对AD进行身份验证的提供程序,虽然没有包含任何内容可以为你提供组成员资格支持(尽管如果你愿意的话,实现起来非常简单,我可以提供一个样本)。

The real issue here is if you want to use AD groups to define permissions within each app, yes? 这里真正的问题是,如果您想使用AD组在每个应用程序中定义权限,是吗?

If so then you do have the option of creating your own RoleProvider for ASP.NET that can also be used by WinForms and WPF apps via ApplicationServices. 如果是这样,那么您可以选择创建自己的ASP.NET RoleProvider,也可以通过ApplicationServices由WinForms和WPF应用程序使用。 This RoleProvider could link the ID of the user in AD to groups/roles per app which you can store in your own custom database, which also allows each app to allow administration of these roles without requiring these admins to have extra privileges in AD. 此RoleProvider可以将AD中用户的ID链接到每个应用程序的组/角色,您可以将其存储在自己的自定义数据库中,这也允许每个应用程序允许管理这些角色,而无需这些管理员在AD中拥有额外的权限。

If you want you can also have an override and combine app roles with AD groups, so if they're in some global "Admin" group in AD they get full permission in the App regardless of App role membership. 如果需要,您还可以覆盖并将应用程序角色与AD组合在一起,因此,如果他们在AD中的某个全局“管理员”组中,则无论App角色成员身份如何,他们都会获得应用程序的完全权限。 Conversely if they have either a group or property in AD to say they've been fired you could ignore all App role membership and restrict all access (since HR probably wouldn't remove them from each and every app, assuming they even know about them all!). 相反,如果他们在AD中有一个团体或财产说他们被解雇了你可以忽略所有App角色成员资格并限制所有访问(因为HR可能不会从每个应用程序中删除它们,假设他们甚至知道它们所有!)。

Sample code added as requested: 根据要求添加示例代码:

NOTE: based on this original work http://www.codeproject.com/Articles/28546/Active-Directory-Roles-Provider 注意:基于这项原创作品http://www.codeproject.com/Articles/28546/Active-Directory-Roles-Provider

For your ActiveDirectoryMembershipProvider you only need to implement the ValidateUser method, although you could implement more if you desired, the new AccountManagement namespace makes this trivial: 对于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;
}

For your role provider it's a little bit more work, there's some key issues we discovered while searching google such as groups you want to exclude, users you want to exclude etc. 对于您的角色提供者来说,这是一项更多的工作,我们在搜索谷歌时发现了一些关键问题,例如您要排除的群组,您要排除的用户等。

It's probably worth a full blog post, but this should help you get started, it's caching lookups in Session variables, just as a sample of how you could improve performance (since a full Cache sample would be too long). 它可能值得一篇完整的博客文章,但这应该可以帮助你入门,它可以在Session变量中缓存查找,就像你可以如何提高性能一样(因为完整的Cache样本会太长)。

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 );
            }
        }
    }
}

A sample config sub-section entry for this would be as follows: 此示例配置子节条目如下:

<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>

Whew, that's a lot of code! 哇,这是很多代码!

PS: Core parts of the Role Provider above are based on another person's work, I don't have the link handy but we found it via Google, so partial credit to that person for the original. PS:上面的角色提供者的核心部分是基于另一个人的工作,我没有方便的链接,但我们通过谷歌找到它,因此对该人的原始部分信用。 We modified it heavily to use LINQ and to get rid of the need for a database for caching. 我们对它进行了大量修改以使用LINQ并且无需数据库进行缓存。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM