65.9K
CodeProject 正在变化。 阅读更多。
Home

自定义成员资格提供程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (68投票s)

2011 年 3 月 8 日

CPOL

11分钟阅读

viewsIcon

476857

downloadIcon

19452

本文重点介绍如何为 ASP.NET MVC 应用程序实现表单身份验证。

引言

身份验证是每个 Web 应用程序不可或缺的一部分。有多种方法可以为您的网站提供身份验证支持。但我个人认为 ASP.NET 的身份验证模型非常适合此目的。ASP.NET 支持多种身份验证模型,例如表单、Windows、Passport 等。使用这些方法之一非常简单。本文重点介绍如何为 ASP.NET MVC 应用程序实现表单身份验证。MembershipProvider 类是本文的基础。因此,首先,我将简要解释如何使用内置的成员资格提供程序。之后,我将详细介绍如何实现自定义成员资格提供程序。在本文的最后,我将确保您拥有一个完全正常运行的、实现了授权的应用程序。

本文的第 2 部分可在此处找到。
本文的第 3 部分(自定义角色提供程序)可在此处找到。

背景 - 使用默认成员资格提供程序

使用 ASP.NET 自带的默认成员资格提供程序既简单又直接。要开始使用默认成员资格提供程序,只需创建一个 ASP.NET MVC 2 Web 应用程序(如果已安装,则为 3)。请记住不要选择 ASP.NET MVC 2 空 Web 应用程序。完成此步骤后,您现在就拥有了一个 ASP.NET MVC 2 应用程序,它具有表单身份验证、一些视图和相关控制器等基本要求。由于本文是关于成员资格提供程序的,因此我不会详细介绍文件夹结构或一般的 MVC 体系结构。

主要重点在于以下文件——AccountController.csweb.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_Usersaspnet_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,如果您在表定义中注意到,已设置为自动递增。这已通过 IsDbGeneratedUserID 列指定。

[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。因此,AccountMembershipServiceCustomMembershipProvider 之间的链接已经建立。因此,一旦您完成了 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 个属性——ValidateUserCreateUserGetUser 以及属性 MinRequiredPasswordLengthRequiresUniqueEmailGetMD5Hash 是用于计算用户输入的密码的 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 属性来“实际”使用我们构建的功能。

CustomMembershipProviders/init.jpg

以下是更新后的代码

[Authorize]
public ActionResult Protected()
{
    return View();
}

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

CustomMembershipProviders/unauth.jpg

CustomMembershipProviders/post.jpg

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.csApplication_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 版发布

© . All rights reserved.