自定义成员资格提供程序






4.78/5 (68投票s)
本文重点介绍如何为 ASP.NET MVC 应用程序实现表单身份验证。
- 下载源代码 (mvc 2) - 291.24 KB
- 下载源代码 (mvc 3 & entity framework) - 1.46 MB
- 新的 MVC 3 和 Entity Framework 项目的 GitHub 链接
引言
身份验证是每个 Web 应用程序不可或缺的一部分。有多种方法可以为您的网站提供身份验证支持。但我个人认为 ASP.NET 的身份验证模型非常适合此目的。ASP.NET 支持多种身份验证模型,例如表单、Windows、Passport 等。使用这些方法之一非常简单。本文重点介绍如何为 ASP.NET MVC 应用程序实现表单身份验证。MembershipProvider
类是本文的基础。因此,首先,我将简要解释如何使用内置的成员资格提供程序。之后,我将详细介绍如何实现自定义成员资格提供程序。在本文的最后,我将确保您拥有一个完全正常运行的、实现了授权的应用程序。
背景 - 使用默认成员资格提供程序
使用 ASP.NET 自带的默认成员资格提供程序既简单又直接。要开始使用默认成员资格提供程序,只需创建一个 ASP.NET MVC 2 Web 应用程序(如果已安装,则为 3)。请记住不要选择 ASP.NET MVC 2 空 Web 应用程序。完成此步骤后,您现在就拥有了一个 ASP.NET MVC 2 应用程序,它具有表单身份验证、一些视图和相关控制器等基本要求。由于本文是关于成员资格提供程序的,因此我不会详细介绍文件夹结构或一般的 MVC 体系结构。
主要重点在于以下文件——AccountController.cs 和 web.config。以下是 web.config 文件中需要我们注意的部分。我们使用 <authentication />
元素指示 ASP.NET 服务器使用表单身份验证。Mode 是一个属性,指示类型和可能的值,包括表单、Windows、Passport 和无。本文是关于表单身份验证的,因此模式设置为“forms”,<forms />
元素指示登录 URL 和超时。本文只关注成员资格部分。将来,我计划将其扩展到包括角色、配置文件等。因此这些部分没有显示在 web.config 文件中。因此,<membership />
元素指示正在使用默认成员资格提供程序。<add />
元素中的一个重要属性是 connectionStringName
属性。它指向将保存成员资格信息的数据库连接字符串。由于这是一个默认应用程序,我让它使用默认连接字符串,即 aspnetdb.mdf 数据库。此 mdf 文件将在您第一次运行示例应用程序时创建。
<connectionStrings>
<add name="ApplicationServices"
connectionString="data source=.\SQLEXPRESS;Integrated Security=SSPI;
AttachDBFilename=|DataDirectory|aspnetdb.mdf;User Instance=true"
providerName="System.Data.SqlClient" />
</connectionStrings>
<authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>
<membership>
<providers>
<clear/>
<add name="AspNetSqlMembershipProvider"
type="System.Web.Security.SqlMembershipProvider"
connectionStringName="ApplicationServices"
enablePasswordRetrieval="false"
enablePasswordReset="true"
requiresQuestionAndAnswer="false"
requiresUniqueEmail="false"
maxInvalidPasswordAttempts="5"
minRequiredPasswordLength="6"
minRequiredNonalphanumericCharacters="0"
passwordAttemptWindow="10"
applicationName="/" />
</providers>
</membership>
事不宜迟,如果您运行应用程序,您将能够看到带有登录链接的主页,如下图所示。单击它将带您进入登录页面。登录页面中有一个注册链接,允许您使用您选择的用户名和密码进行注册。很简单,不是吗?现在,如果您导航到 App_Db 文件夹,您将能够注意到 aspnetdb.mdf 文件已创建。由于您使用了默认成员资格提供程序,ASP.NET 使用其自己的表结构,其中包含 aspnet_Users
、aspnet_Membership
等表,这些表包含成员资格信息。现在您拥有一个带有身份验证功能的 MVC 网站的工作版本。但这并没有结束。现在让我们看看如何实现自定义成员资格提供程序,而不是使用默认成员资格提供程序。
自定义成员资格提供程序
从这一点开始,您将看到大量的代码而不仅仅是描述。如果您有问题,请随时在下面的评论部分提出。在本文的开头,有一个部分列出了与本文相关的下载。其中列出了下载实现自定义成员资格提供程序的整个项目的链接。我将以此作为参考,以便您更容易理解。
实现自定义成员资格提供程序的第一步是创建一个扩展 MembershipProvider
类的类。这个类有一长串的方法。此时,重点是 3 个方法和 2 个属性——一个用于验证用户,一个用于按用户名查找用户,一个用于注册新用户,以及返回最小密码长度和是否允许重复电子邮件的属性。首先,创建一个新的 ASP.NET MVC 2 项目(不是空项目),并将其命名为 CustomMembershipProvider
。然后,在您的 Models 文件夹中,创建一个名为 CustomMembershipProvider
的类。这个类将扩展 abstract
MembershipProvider
类。下面是 CustomMembershipProvider
提供程序类,只列出了我们需要的 3 个方法。MembershipProvider
包含在 System.Web.Security
中,因此您可能需要添加对该命名空间的引用。
提示:要添加所有要实现的方法,请将光标放在 MembershipProvider
单词的开头(或结尾),按 Ctrl + .,然后选择 Implement abstract
类 'MembershipProvider
'。
public class CustomMembershipProvider : MembershipProvider
{
public override MembershipUser CreateUser(string username,
string password, string email, string passwordQuestion,
string passwordAnswer, bool isApproved,
object providerUserKey, out MembershipCreateStatus status)
{
throw new NotImplementedException();
}
public override MembershipUser GetUser(string username, bool userIsOnline)
{
throw new NotImplementedException();
}
public override bool ValidateUser(string username, string password)
{
throw new NotImplementedException();
}
public override int MinRequiredPasswordLength
{
get { throw new NotImplementedException(); }
}
public override bool RequiresUniqueEmail
{
get { throw new NotImplementedException(); }
}
}
我们稍后再讨论实现部分。现在打开 web.config 文件,将 <connectionStrings />
元素中 <add />
元素下的 connectionString
属性值更改为指向您的数据库。然后保持 <authentication />
元素不变,并将默认的 <membership />
元素替换为以下内容。以下 web.config 假定您已将项目命名为 CustomMembershipProvider
,并将 CustomMembershipProvider.cs 文件添加到 Models 文件夹。
关注点
<connectionStrings>
<add name="ApplicationServices"
connectionString="Server=your_server;Database=your_db;
Uid=your_user_name;Pwd=your_password;"
providerName="System.Data.SqlClient" />
</connectionStrings>
<authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>
<membership defaultProvider="CustomMembershipProvider">
<providers>
<clear/>
<add name="CustomMembershipProvider"
type="CustomMembership.Models.CustomMembershipProvider"
connectionStringName="AppDb"
enablePasswordRetrieval="false"
enablePasswordReset="true"
requiresQuestionAndAnswer="false"
requiresUniqueEmail="false"
maxInvalidPasswordAttempts="5"
minRequiredPasswordLength="6"
minRequiredNonalphanumericCharacters="0"
passwordAttemptWindow="10"
applicationName="/" />
</providers>
如果您注意到,您将能够看到一些差异。首先,在defaultProvider
”属性来指定这是默认提供程序,而不是 MembershipProvider
。然后,在type
属性是我们创建的类的完全限定名称(在“Models”文件夹中)。下一步是创建一个表来保存您的用户。下面是您可以在数据库中运行以创建表的 create
脚本。
CREATE TABLE [dbo].[Users](
[UserID] [int] IDENTITY(1,1) NOT NULL,
[UserName] [varchar](50) NOT NULL,
[Password] [varchar](50) NOT NULL,
[UserEmailAddress] [varchar](50) NOT NULL,
CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED
(
[UserID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
现在,我们需要创建一个类来表示此表,以便我们进行操作。下面的代码列表表示该类。我想代码是自解释的。但是,一些细节仍然不会有损。Table
属性指示此类表示一个名为“Users
”的表。上面表中的每一列都存在于类中,并用 Column
属性修饰。UserID
,如果您在表定义中注意到,已设置为自动递增。这已通过 IsDbGenerated
为 UserID
列指定。
[Table(Name="Users")]
public class UserObj
{
[Column(IsPrimaryKey=true, IsDbGenerated = true, AutoSync=AutoSync.OnInsert)]
public int UserID { get; set; }
[Column] public string UserName { get; set; }
[Column] public string Password { get; set; }
[Column] public string UserEmailAddress { get; set; }
}
管理用户的创建/验证
下一步将是创建一个用户存储库,该存储库将执行用户的实际创建/验证。此类列于下方。CustomMembershipProvider
使用此存储库来验证 (GetUserObjByUsername
) 和创建用户 (RegisterUser
)。
public class User
{
private Table<UserObj> usersTable;
private DataContext context;
public User()
{
string connectionString =
ConfigurationManager.ConnectionStrings["AppDb"].ConnectionString;
context = new DataContext(connectionString);
usersTable = context.GetTable<UserObj>();
}
public UserObj GetUserObjByUserName(string userName, string passWord)
{
UserObj user = usersTable.SingleOrDefault(
u => u.UserName == userName && u.Password == passWord);
return user;
}
public UserObj GetUserObjByUserName(string userName)
{
UserObj user = usersTable.SingleOrDefault(u => u.UserName == userName);
return user;
}
public IEnumerable<UserObj> GetAllUsers()
{
return usersTable.AsEnumerable();
}
public int RegisterUser(UserObj userObj)
{
UserObj user = new UserObj();
user.UserName = userObj.UserName;
user.Password = userObj.Password;
user.UserEmailAddress = userObj.UserEmailAddress;
usersTable.InsertOnSubmit(user);
context.SubmitChanges();
return user.UserID;
}
}
现在我们有了执行实际工作的存储库,让我们回到 CustomMembershipProvider
类。需要注意的重要一点是,使用此 CustomMembershipProvider
的类已在 Models 文件夹中可用 - AccountModels.cs 文件。如果您注意到该文件,您将能够找到以下几行
public class AccountMembershipService : IMembershipService
{
private readonly MembershipProvider _provider;
-- cut for brevity --
}
如果您还记得,在 web.config 中,我们已经将 CustomMembershipProvider
定义为 defaultProvider
。因此,AccountMembershipService
和 CustomMembershipProvider
之间的链接已经建立。因此,一旦您完成了 CustomMembershipProvider
类中所需的方法,您就可以开始了!现在,下面是一个 CustomMembershipProvider
类的版本,其中未实现的方法已实现。
public class CustomMembershipProvider : MembershipProvider
{
public override MembershipUser CreateUser(string username, string password,
string email, string passwordQuestion, string passwordAnswer,
bool isApproved, object providerUserKey, out MembershipCreateStatus status)
{
ValidatePasswordEventArgs args =
new ValidatePasswordEventArgs(username, password, true);
OnValidatingPassword(args);
if (args.Cancel)
{
status = MembershipCreateStatus.InvalidPassword;
return null;
}
if (RequiresUniqueEmail && GetUserNameByEmail(email) != string.Empty)
{
status = MembershipCreateStatus.DuplicateEmail;
return null;
}
MembershipUser user = GetUser(username, true);
if (user == null)
{
UserObj userObj = new UserObj();
userObj.UserName = username;
userObj.Password = GetMD5Hash(password);
userObj.UserEmailAddress = email;
User userRep = new User();
userRep.RegisterUser(userObj);
status = MembershipCreateStatus.Success;
return GetUser(username, true);
}
else
{
status = MembershipCreateStatus.DuplicateUserName;
}
return null;
}
public override MembershipUser GetUser(string username, bool userIsOnline)
{
User userRep = new User();
UserObj user = userRep.GetAllUsers().SingleOrDefault
(u => u.UserName == username);
if (user != null)
{
MembershipUser memUser = new MembershipUser("CustomMembershipProvider",
username, user.UserID, user.UserEmailAddress,
string.Empty, string.Empty,
true, false, DateTime.MinValue,
DateTime.MinValue,
DateTime.MinValue,
DateTime.Now, DateTime.Now);
return memUser;
}
return null;
}
public override bool ValidateUser(string username, string password)
{
string sha1Pswd = GetMD5Hash(password);
User user = new User();
UserObj userObj = user.GetUserObjByUserName(username, sha1Pswd);
if (userObj != null)
return true;
return false;
}
public override int MinRequiredPasswordLength
{
get { return 6; }
}
public override bool RequiresUniqueEmail
{
// In a real application, you will essentially have to return true
// and implement the GetUserNameByEmail method to identify duplicates
get { return false; }
}
public static string GetMD5Hash(string value)
{
MD5 md5Hasher = MD5.Create();
byte[] data = md5Hasher.ComputeHash(Encoding.Default.GetBytes(value));
StringBuilder sBuilder = new StringBuilder();
for (int i = 0; i < data.Length; i++)
{
sBuilder.Append(data[i].ToString("x2"));
}
return sBuilder.ToString();
}
}
如果您注意到,我们至少只需要实现 3 个方法和 2 个属性——ValidateUser
、CreateUser
、GetUser
以及属性 MinRequiredPasswordLength
、RequiresUniqueEmail
。GetMD5Hash
是用于计算用户输入的密码的 MD5 散列的方法。MD5 不是一种安全的算法,因此我建议您使用另一种散列(/加密)算法来保存密码。千万,千万不要将密码存储为明文!此外,您还会注意到我们正在使用前面步骤中创建的用户存储库。
最后步骤
现在我们已经走到这一步,首先导航到注册链接,注册,然后返回登录屏幕并登录。如果您能够成功登录,您就完成了您预期目标的 75%!但是,如果您注意到,您将能够访问“Views”部分中的每个页面,无论您是否经过身份验证。别担心,身份验证正在工作。但是您需要添加另一个属性来阻止这种行为。让我们分两步完成此操作。在此之前,请从应用程序注销。首先,打开 Controllers 文件夹中的 HomeController.cs 文件并添加以下代码
public ActionResult Protected()
{
return View();
}
现在右键单击 Protected
方法内部并选择“添加视图...”。这将在 Views/Home 文件夹中添加一个名为 Protected.aspx 的视图。将以下内容添加到此文件
<%@ Page Title="" Language="C#"
MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<dynamic>" %>
<asp:Content ID="Content1"
ContentPlaceHolderID="TitleContent" runat="server">
Protected
</asp:Content>
<asp:Content ID="Content2"
ContentPlaceHolderID="MainContent" runat="server">
<h2>Protected</h2>
This is a protected page!
</asp:Content>
然后在 Views/Home 文件夹中的 Home.aspx 页面中,添加以下内容
<%= Html.ActionLink("protected page","Protected") %>
构建应用程序并运行它。转到主页,您应该能够看到受保护的页面链接。如果您单击它,即使您未经过身份验证,您也能够看到该页面!因此,这里是真正保护此页面的关键。使用 Authorize
属性来“实际”使用我们构建的功能。

以下是更新后的代码
[Authorize]
public ActionResult Protected()
{
return View();
}
现在,构建应用程序。如果您尝试刷新页面,您将被带到登录页面。如果您注意到 URL,您会看到一个名为 returnUrl
的查询参数,它被设置为 /Home/Protected。因此,当您登录时,您将被重定向到受保护的页面。这是您真正实现(并使用)自定义身份验证的时候!

Entity Framework (Code First) 基于实现的快速指南
如果您注意到,在讨论/评论部分,总是有人询问如何使用 MVC 3 和 Entity Framework (Code First) 实现项目。因此,我认为如果我更新文章,提供 MVC 3/EF 实现,对很多人来说肯定会有帮助。文章前面给出的相同表定义仍然可以用于此项目。为了以防万一,我还添加了一个 Setup.sql
文件,其中包含用于创建表的脚本。下载部分现在有一个新添加——Custom-Membership-Providers-Using_Entity-Framework.zip,它正好满足了很多人的需求!
关于这个新添加的一些话...
这个项目包含 User
类,它代表系统中的一个用户。这是一个普通的旧 c# 对象,它将代表 Users
表中的一行。
public class User
{
public int UserID { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public string UserEmailAddress { get; set; }
}
现在,让我们添加将管理表中条目的数据上下文。如下所示
public class UsersContext : DbContext
{
public DbSet<user> Users { get; set; }
// Helper methods. User can also directly access "Users" property
public void AddUser(User user)
{
Users.Add(user);
SaveChanges();
}
public User GetUser(string userName)
{
var user = Users.SingleOrDefault(u => u.UserName == userName);
return user;
}
public User GetUser(string userName, string password)
{
var user = Users.SingleOrDefault(u => u.UserName == userName && u.Password == password);
return user;
}
}
所有使用 Entity Framework 的数据上下文都必须继承自 DbContext
类。每个表都通过 DbSet
属性公开。在本例中是 Users
表。我还添加了一些辅助方法来添加新用户和获取用户。现在,自定义成员资格提供程序可以利用此数据上下文来操作 Users
表,如下所示
public override bool ValidateUser(string username, string password)
{
var md5Hash = GetMd5Hash(password);
using (var usersContext = new UsersContext())
{
var requiredUser = usersContext.GetUser(username, md5Hash);
return requiredUser != null;
}
}
请注意,在第 5 行,创建了一个 UsersContext
实例以验证输入的用户名和密码是否有效。Entity Framework 的一个特点是,每当应用程序启动时,它都会尝试再次创建数据库。为了阻止 Entity Framework 执行此操作,必须在 Global.asax.cs
的 Application_Start
方法中执行以下操作。请注意下面代码片段中的第 8 行,通过传入 NULL
来调用 Database.SetInitializer
泛型方法(带类型参数 UsersContext
)。这会阻止数据库每次都初始化/删除。
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
Database.SetInitializer<UsersContext>(null);
}
我还修改了布局页面,提供了指向受保护页面的链接。未经登录的用户将无法查看此页面,因为它受 Authorize
属性的保护。只需下载项目并开始探索!希望这次小更新能有所帮助!谢谢!
请随时在下面的“评论和讨论”部分告诉我您的意见。
历史
文章第 2 版发布 - 增加了使用 mvc 3 和 entity framework 的相同项目的新下载以及相应的讨论
文章第 1 版发布