使用 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 并摆脱了对数据库进行缓存的需求。