ASP.NET成员资格功能减少了您必须编写的代码数量,以便对用户进行身份验证并存储其凭据。引用MSDN:
“ ASP.NET会员资格为您提供了一种内置的验证和存储用户凭据的方式。您将ASP.NET成员资格与表单身份验证和 /或ASP.NET登录控件一起用于身份验证用户。”
需要在Web.config配置文件中指定会员提供商。您可以使用自己的自定义提供商或使用.NET Framework汇发的默认提供商之一,例如SQLMembersHippRovider提供商。
所有相关的数据都存储在ASP.NET成员系统使用的一组表中。在大多数情况下,您每个Web应用程序都使用一组表。但是,您还可以重复使用相同的集合来存储多个Web应用程序的用户凭据。这有效地使您可以创建一个可作为门户的Web应用程序,使您可以登录这些“虚拟”应用程序之一。
但是,这是不可能的。在支持此类动态应用程序之前,您需要进行一些工作。
让我们开始...
让我们首先检查以下web.config条目:
列表1 -Web.config asp.net成员资格配置
< membership defaultProvider = " SqlProvider " >
< providers >
< clear />
< add name = " SqlProvider "
type = " System.Web.Security.SqlMembershipProvider "
connectionStringName = " YourConnectionStringName "
applicationName = " MyApplication "
... />
</ providers >
</ membership >这些设置设置了您的Web应用程序,以使用SQLMembersHippRovider来管理您的用户凭据。与您的用户相关的所有数据都存储在默认的ASP.NET成员表中。
评论:本文不是ASP.NET成员的入门,您应该熟悉它。使用ASP.NET SQL Server注册工具(ASPNET_REGSQL.EXE)来设置数据库。
如前所述,您可以将多个Web应用程序的用户凭据存储在同一表表中。每个Web应用程序通过在我所说的应用程序上下文中对其数据进行分区。此应用程序上下文由会员提供商的应用名称属性定义。如您在清单1中所见,应用程序名称为“ myApplication”。
SQLMembersHippRovider使用此设置来确定应在哪个上下文中操作,所有与此应用程序名称相关的用户数据将由ASP.NET成员资格库和登录控件访问。
不幸的是,此设置在web.config中静态定义。您无法在运行时轻松更改它。如果您想支持动态应用程序,则需要一种控制提供商应用名称属性值的方法。解决方案在于创建您自己的ASP.NET会员资格提供商。
SQLMembersHippRovider从Web.config文件中包含的配置设置中检索其应用名称属性的值。我们想更改此属性返回的值的价值。
登录用户时,应明确他的意图,换句话说,他必须指定他想访问的虚拟应用程序。为此,我要求用户不仅指定他的用户名和密码,还指定他要访问的虚拟应用程序。从用户的角度来看,我称此虚拟应用程序为域。
例如,假设您已经建立了一个称为ASPNETMEMBERHIP的SQL Server数据库,该数据库包含ASP.NET成员表。该数据库包含以下在Aspnet_applications表中定义的虚拟应用程序:
每个虚拟应用程序都包含用户名“ CGEERS”的用户。现在,如果我想登录Northwind应用程序,我必须将用户名输入“ Northwind cgeers”。对于AdventureWorks应用程序,这将是“ AdventureWorks cgeers”。
用户名之前的零件是应用程序名称,或者就用户而言,这是他必须输入的域,以指定他要访问的虚拟应用程序。域和用户名被后斜切隔开。
当用户使用ASP.NET登录控件登录时,我们必须提取他输入的域并将其存储在我们的自定义会员提供者可以访问的位置中。但是,我们必须考虑到ASP.NET在多线程环境中运行。 ASP.NET收到的每个请求均由单独的线程处理。我们不能仅将域存储在任何位置,其他传入请求可能会覆盖它。必须与单个请求联系在一起。
存储与单个请求相关的上下文信息的理想场所是当前的HTTP上下文。此上下文信息由HTTPContext类封装。
启动Visual Studio并创建一个名为“ Aspnetdynamicapplications”的新空白解决方案。接下来,将一个新的类库项目添加到名为“ cgeers.web.security”的解决方案中。将引用添加到System.configuration和System.Web组件中。我们的自定义提供商将从默认的SQLMembersHippRovider下降,因此需要这些参考。
图1-视觉工作室解决方案

将自动生成的class1.cs文件重命名为dynamicApplicationssqlmembershipprovider.cs,并添加列表2中显示的代码。
清单2 -DynamIcapplicationssqlmembershipprovider
public class DynamicApplicationsSqlMembershipProvider : SqlMembershipProvider
{
#region Fields
private const string ApplicationNameSetting = "ApplicationName" ;
#endregion
public override string ApplicationName
{
get
{
HttpContext context = HttpContext . Current ;
if ( context == null )
{
throw new InvalidOperationException ( "Http context cannot be null." ) ;
}
string applicationName = String . Empty ;
if ( context . Items . Contains ( ApplicationNameSetting ) )
{
if ( ! String . IsNullOrEmpty ( ( string ) context . Items [ ApplicationNameSetting ] ) )
{
applicationName = ( string ) context . Items [ ApplicationNameSetting ] ;
}
}
return applicationName ;
}
set
{
base . ApplicationName = value ;
}
}
}如您所见,我们的自定义会员提供商从默认的SQLMembersHippRovider下降并覆盖了应用程序名称属性。我们并不关心固定器,只关心getter。当提供商读取ApplicationName属性时,我们通过调用HTTPContext类的静电当前属性来获取当前HTTP上下文的处理。获得的HTTPContext对象具有称为项目的密钥/值收集属性。该集合将应用程序名称存储在密钥“ ApplicationName”标识的条目中。此键的名称是您必须在设计时间确定的。
现在,每当我们的自定义会员提供商都需要确定调用应用程序名称的应用名称属性,该应用程序名称属性从当前的HTTP上下文中检索值。这在ASP.NET的多线程环境中起作用,因为每个请求都与其自己的HTTP上下文相关。
现在,我们是一个自定义会员提供商,它从Courte http上下文中检索其应用名称属性的值,我们仍然必须找出一种将应用程序名称实际存储在HTTP上下文中的方法。
用户第一次指定他要访问的虚拟应用程序或<domain><username>是他登录时。我们必须创建自己的登录控件,该控件从ASP.NET登录控件下降。用户身份验证,我们需要提取输入的域并将其保存在当前的HTTP上下文中。
使用类库模板添加一个新的项目,称为“ cgeers.web.ui.webcontrols”的解决方案。添加以下对项目的引用:
接下来删除自动生成的class1.cs文件,并添加一个名为DynamIcapplicationsLogin的新类。由于此类的代码非常漫长,因此我选择将其分解为几个列表。在列出每个列表后,添加了代码的简要说明。
列表3 -DynamicApplicationsLogin控制私人属性
public class DynamicApplicationsLogin : Login
{
#region Fields
private string _fullUserName ;
#endregion
#region Properties
private string ApplicationName
{
get
{
string [ ] data = base . UserName . Split ( @"" . ToCharArray ( ) , 2 ) ;
string applicationName = ( data . Length == 2 ) ? data [ 0 ] : String . Empty ;
return applicationName ;
}
}
private string BaseUserName
{
get
{
string [ ] data = base . UserName . Split ( @"" . ToCharArray ( ) , 2 ) ;
string userName = ( data . Length == 2 ) ? data [ 1 ] : base . UserName ;
return userName ;
}
}
#endregion
// ...
}如您所见,从标准ASP.NET登录控件和一个称为_fullusername的字段和两个私有属性ApplicationName和baseUserName的字段中添加了DynamicApplicationsLogin控件。
由于用户将域和用户名输入相同的文本框控件,因此我们需要将这些零件分开。您可能会猜测,应用程序名称属性提取域,并且baseSername返回用户名。例如,如果用户进入Northwind cgeers,则应用名称属性将返回“ northwind”,并且baseSername属性将返回“ cgeers”。
列表4 -DynamicApplicationsLogin Control Onuthenticate方法
protected override void OnAuthenticate ( AuthenticateEventArgs e )
{
HttpContext context = HttpContext . Current ;
if ( context == null )
{
throw new InvalidOperationException ( "Http context cannot be null." ) ;
}
MembershipProvider provider = Membership . Provider ;
if ( provider == null )
{
throw new InvalidOperationException ( "MembershipProvider cannot be null." ) ;
}
provider = provider as DynamicApplicationsSqlMembershipProvider ;
if ( provider == null )
{
throw new InvalidOperationException (
"The specified MembershipProvider must be of type DynamicApplicationsSqlMembershipProvider." ) ;
}
// Store the application name in the current Http context's items collections
context . Items [ "ApplicationName" ] = ApplicationName ;
// Validate the user
_fullUserName = UserName ;
UserName = BaseUserName ;
base . OnAuthenticate ( e ) ;
}接下来,您需要覆盖登录控件的启用方法,以确保用户在正确的应用程序上下文(虚拟应用程序)中验证。
首先,此方法检索了对当前HTTP上下文的引用,然后检查加载的会员资格提供商是否确实是我们的自定义DynamIcapplicationssqlmembershipprovider。然后将输入的域或应用程序名称存储在当前HTTP上下文的项目集合中,以便我们的会员提供者可以检索其应用名称属性的正确值。
最后但并非最不重要的一点是,用户名属性的值设置为仅通过为其分配基本名称属性来包含用户名。私有_fullusername字段临时存储用户输入的值(域用户名)。您将看到以后如何使用该字段。
应用程序名称存储在HTTP上下文中,并且用户名属性仅包含调用基本实现的用户名,这会导致用户名在正确的应用程序上下文中进行验证。
列表5 -DynamicApplicationsLogin控制OnloginError方法
protected override void OnLoginError ( EventArgs e )
{
UserName = _fullUserName ;
base . OnLoginError ( e ) ;
}覆盖onLoginError方法,然后在调用基本实现之前,将_fullusername字段分配给用户名属性。在对用户进行身份验证时,用户名属性必须仅包含用户名而不是域,但是当身份验证失败时,您需要确保在用户中输入的用户在代表用户名中输入的文本重置为用户名,以将用户重置为用户最初输入的用户。其他原因,如果他进入的域消失了,这可能会使用户感到困惑。
清单6- dynamicApplicationsLogin控制onloggedin方法
protected override void OnLoggedIn ( EventArgs e )
{
UserName = _fullUserName ;
HttpContext context = HttpContext . Current ;
if ( context == null )
{
throw new InvalidOperationException ( "Http context cannot be null." ) ;
}
string userName = BaseUserName ;
MembershipUser user = Membership . GetUser ( userName ) ;
if ( user != null )
{
string userData = String . Format ( "AN={0};" , ApplicationName ) ;
HttpCookie cookie = FormsAuthenticationHelper . StoreUserDataInAuthenticationCookie (
userName , userData , RememberMeSet
) ;
// Manually add the cookie to the Cookies collection
context . Response . Cookies . Add ( cookie ) ;
}
}要完成DynamicApplicationsLogin控制,您需要覆盖OnloggedIn方法。该应用名称存储在FormauthenticationTicket的UserData属性中。表格身份验证使用此身份验证票证以识别身份验证的用户。在这里,我们将其存储在cookie中,并将其添加到当前HTTP上下文的响应中。稍后,您会看到为什么需要。
您可以看一下清单6,您会看到包含表单身份验证票的cookie是由称为formauthenticationhelper的帮助人类创建的。这是一个静态的助手类,仅包含一种方法,即storeuserdatainauthenticationcookie。
在cgeers.web.security项目中添加一个名为formauthenticationhelper的新类,并添加列表7中显示的代码。
清单7- form authenticationhelper
public static class FormsAuthenticationHelper
{
public static HttpCookie StoreUserDataInAuthenticationCookie (
string userName , string userData , bool persistent )
{
if ( String . IsNullOrEmpty ( userName ) )
{
throw new InvalidOperationException ( "UserName cannot be null or empty." ) ;
}
if ( String . IsNullOrEmpty ( userData ) )
{
throw new InvalidOperationException ( "User data cannot be null or empty." ) ;
}
// Create the cookie that contains the forms authentication ticket
HttpCookie cookie = FormsAuthentication . GetAuthCookie ( userName , persistent ) ;
// Get the FormsAuthenticationTicket out of the encrypted cookie
FormsAuthenticationTicket ticket = FormsAuthentication . Decrypt ( cookie . Value ) ;
// Create a new FormsAuthenticationTicket that includes our custom user data
FormsAuthenticationTicket newTicket = new FormsAuthenticationTicket ( ticket . Version ,
ticket . Name ,
ticket . IssueDate ,
ticket . Expiration ,
persistent ,
userData ) ;
// Update the cookie's value to use the encrypted version of our new ticket
cookie . Value = FormsAuthentication . Encrypt ( newTicket ) ;
// Return the cookie
return cookie ;
}
}StoreUserDatainAuthenticationCookie(...)方法检索包含表单身份验证票的指定用户(用户名参数)的cookie。接下来,将cookie解密以访问formauthenticationticket对象。然后,基于旧票证创建一个新的FormaUthenticationTicket,并且在此票证中存储了用户数据参数中包含的值。作为最后一步,新的FormsAuthenticationTicket对象存储在加密的cookie中。
备注:DynamicApplicationsLogin Control商店将此cookie发送给用户,因此客户需要支持cookie!
让我们概括到目前为止我们拥有的东西。
因此,我们有自定义会员资格提供商,可以根据请求确定要在此操作的应用程序上下文,并且我们拥有一个自定义登录控件,该控件可以通过要求用户在用户名中使用域(=应用程序名称)来标识此上下文。
如果您的Web应用程序仅由登录页面组成,则可以很好地工作。请记住,自定义会员提供商通过阅读当前HTTP上下文中存储的应用程序名称来确定应用程序上下文。通过您提出的每个请求,将创建一个新的HTTP上下文。登录应用程序名称时,将明确存储在HTTP上下文中。但是,对于任何后续请求,情况都不是这样。不知何故,我们必须确保在用户登录后,使用应用程序名称正确初始化了HTTP上下文。
如前所述,DynamicApplicationLogin控件成功登录后,向用户发送了加密的cookie。此Cookie包含应用程序名称,并与每个后续请求一起发送。 VOILA,现在我们需要的是一种将某些逻辑插入ASP.NET请求管道的方法,以读取此Cookie的内容并将检索到的应用程序名称存储在HTTP上下文中。
HTTP模块非常适合这种情况。每个请求都调用HTTP模块,并在处理请求之前和之后运行。
将一个名为DynamIcapplicationsModule的新类添加到cgeers.web.security项目。此类需要实现IHTTPMODULE接口。此HTTP模块的代码显示在清单8中。
清单8 -DynamicApplicationsModule
public class DynamicApplicationsModule : IHttpModule
{
#region Fields
private const string ApplicationNameSetting = "ApplicationName" ;
#endregion
#region IHttpModule Members
public void Dispose ( )
{ }
public void Init ( HttpApplication context )
{
context . AuthenticateRequest += DetermineApplicationName ;
}
#endregion
private static void DetermineApplicationName ( object sender , EventArgs e )
{
// Access the current Http application.
HttpApplication application = sender as HttpApplication ;
if ( application == null )
{
throw new InvalidOperationException ( "Http application cannot be null." ) ;
}
// Get the HttpContext for the current request.
HttpContext context = application . Context ;
if ( context == null )
{
throw new InvalidOperationException ( "Http context cannot be null." ) ;
}
// Read the application name stored in the FormsAuthenticationTicket
string applicationName = String . Empty ;
if ( context . Request . IsAuthenticated )
{
FormsIdentity identity = context . User . Identity as FormsIdentity ;
if ( identity != null )
{
FormsAuthenticationTicket ticket = identity . Ticket ;
if ( ticket != null )
{
applicationName = ticket . GetApplicationName ( ) ;
}
}
}
// Store the application name in the Items collection of the per-request http context.
// Storing it in the session state is not an option as the session is not available at this
// time. It is only available when the Http application triggers the AcquireRequestState event.
context . Items [ ApplicationNameSetting ] = applicationName ;
}
}INIT(...)方法初始化了HTTP模块并准备处理请求。在这里,HTTPApplication的AuthentIcateRequest事件已挂在dectiNeApplicationName事件处理程序上。
decitioneapplicationname(...)事件处理程序检查其是否处理已验证的请求,如果这样检索客户端发送的FormaAthentication票。
该票证标识操作的应用程序上下文。使用formauthenticationticket的getApplicationName()方法从票证中提取应用程序名称。这是一种扩展方法,我将在下一节中解决。
最后,从票证中提取应用程序名称后,它将存储在HTTP上下文中,以便我们的自定义会员提供者可以阅读它。
将一个名为formauthenticationTicketextensions的新类添加到cgeers.web.security项目中,并添加清单9中显示的代码。
列表9- formauthenticationticketextensions
internal static class FormsAuthenticationTicketExtensions
{
public static string GetApplicationName ( this FormsAuthenticationTicket ticket )
{
// Check if the application name (AN=) is stored in the ticket's userdata.
string applicationName = String . Empty ;
List < string > settings = new List < string > ( ticket . UserData . Split ( ';' ) ) ;
foreach ( string s in settings )
{
string setting = s . Trim ( ) ;
if ( setting . StartsWith ( "AN=" ) )
{
int startIndex = setting . IndexOf ( "AN=" ) + 3 ;
applicationName = setting . Substring ( startIndex ) ;
break ;
}
}
return applicationName ;
}
}如您在列表6中所见,DynamicApplicationLogin控件将应用程序名称保存在FormsAuthenticationTicket的UserData属性中,该名称是通过前缀“ AN =”之前的。因此,此扩展方法在从formauthenticationticket检索应用程序名称时需要考虑到这一点。
注释:随时可以改进票务中的应用程序名称的方式,但请记住为DynamicApplicationsLogin Control和此扩展方法实现相同的系统。
与DynamicApplicationsLogin控件相似,我创建了从ASP.NET passwordRecovery和ChangePassword控件下降的控件。如果您希望用户能够检索他的密码和/或更改它,则需要使用这些控件。他们确保在正确的应用程序上下文中执行所有内容。
该实现与DynamicApplicationsLogin控件的实现非常相似,因此我不会在此处列出这些控件的代码。如果要查看这些控件的源代码,请下载本文随附的源代码。您可以在cgeers.web.ui.webcontrols项目中找到这些控件。
本文随附的源代码还包含一个演示Web应用程序项目,该项目演示了DynamicApplicationssqlmembershipprovider,新的登录控件,我们的自定义HTTP模块...等等。
图2-视觉工作室解决方案

为了运行此演示应用程序,请按照以下步骤:
遵循这些步骤后,您应该准备运行演示Web应用程序。您创建了一个数据库,该数据库包含两个应用程序和一个用户。
启动Web应用程序时,您可以通过指定应用程序名称(域)后面登录每个应用程序,然后使用后斜线和用户名。当然,也需要用户密码。
我们创建的登录控件会照顾其余的,并确保您在正确的应用程序上下文中运行。随后使用ASP.NET成员库的任何后续呼叫都将触发自定义会员提供商返回正确的应用程序名称,从而确保这些调用在正确的应用程序上下文中运行。
备注:演示Web应用程序的Web.config文件包含注释,这些注释指定了您需要做的所有操作以正确设置应用程序。请务必检查一下。
本文向您展示了如何使用ASP.NET成员系统来创建一个门户应用程序,该应用程序允许用户登录到许多虚拟应用程序之一。
意识到这一点的第一个关键是创建一个自定义会员资格提供商,该提供商可以通过从与单个请求相关的存储位置检索应用名称属性的值来确保您在正确的应用程序上下文中运行。
通过创建从标准ASP.NET登录控件下降的自定义登录控件,我们可以确定用户想要访问的应用程序。
通过将这些信息封装在加密的cookie中,我们可以自动识别用户想要为其每个请求访问的应用程序。