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

自定义角色提供程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (32投票s)

2013年6月16日

CPOL

14分钟阅读

viewsIcon

208651

介绍在ASP.NET MVC 3应用程序中使用Entity Framework进行ORM的自定义角色提供程序。

Default home page

引言

就像身份验证对于Web应用程序至关重要一样,角色也出于多种原因而必不可少。例如,角色可以用来限制应用程序的某些功能的可用性,仅限于特定用户组。本文完全基于ASP.NET MVC提供的RoleProvider抽象类(MSDN)。阅读完本文后,您将能够使用文章第一部分提供的示例应用程序(下载)并根据三个角色进行页面划分 - “超级管理员”,“管理员”和“作者”。

登录的默认用户名/密码(超级管理员用户):administrator / administrator

背景

我之前写过一篇关于使用自定义成员资格提供程序的文章,并收到了很多评论。其中一条评论是跟进一篇关于自定义角色提供程序的文章。本文可被视为我之前关于自定义成员资格提供程序的文章的“续篇”!我使用了ASP.NET MVC 3来解释自定义角色提供程序,并使用Entity Framework作为数据层。

关于用户和角色的几句话

为了控制对某个操作方法的访问,您可以使用Authorize属性,如下所示

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

Authorize属性仅控制对特定操作方法(在本例中为Index)的访问。如果用户已登录,则他们可以看到页面。如果未登录,则用户会被重定向到web.config文件中指定或通过代码指定的登录页面。您可以通过传入User参数来限制仅特定用户访问某个页面,如下所示。在这种情况下,此页面的访问仅限于用户名为admin的用户。

[Authorize(Users = "admin")]
public ActionResult Index()
{
    return View();
}

如果另一个用户(例如karthik)需要访问此页面,则必须将Users参数更改为包含第二个用户,如下所示

[Authorize(Users = "admin,karthik")]
public ActionResult Index()
{
    return View();
}

从修改后的版本来看,我认为这种方法扩展性不好。如果有一百个用户需要访问此页面怎么办?如果新注册的用户需要访问此页面怎么办?手动更改属性以包含我们需要的用户简直是不可思议!请注意,每次更改此设置都需要重新生成!那么,我们该怎么办?“角色”是答案!现在让我们进入本文最有趣的部分:)

先决条件 - 数据库和数据访问层

本节只是主要内容的前奏!要准备您的数据库,请在MS SQL Server中创建一个新数据库,并在下载的源代码中运行Setup.sql。这将创建三个表

  • Users - 用于存储用户
    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]
  • Roles - 用于存储此应用程序有效的角色
    CREATE TABLE [dbo].[Roles](
        [RoleId] [smallint] NOT NULL,
        [RoleName] [varchar](50) NOT NULL,
        [RoleDescription] [varchar](255) NOT NULL,
     CONSTRAINT [PK_Roles] PRIMARY KEY CLUSTERED 
    (
        [RoleId] ASC
    )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, 
      IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
    ) ON [PRIMARY]
    
    GO
    
    /* Roles table entries */
    INSERT INTO dbo.Roles VALUES(0,'SuperAdmin','Super Admin');
    INSERT INTO dbo.Roles VALUES(1, 'Admin', 'Administrator');
    INSERT INTO dbo.Roles VALUES(2, 'Author', 'Blog Author');
  • UserRoles - 用于存储与用户关联的角色
    CREATE TABLE [dbo].[UserRoles](
        [UserRoleId] [int] IDENTITY(1,1) NOT NULL,
        [UserId] [int] NOT NULL,
        [RoleId] [smallint] NOT NULL,
     CONSTRAINT [PK_UserRoles] PRIMARY KEY CLUSTERED 
    (
        [UserRoleId] ASC
    )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, 
      IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
    ) ON [PRIMARY]
    
    GO
    
    ALTER TABLE [dbo].[UserRoles]  
       WITH CHECK ADD  CONSTRAINT [FK_UserRoles_Roles] FOREIGN KEY([RoleId])
    REFERENCES [dbo].[Roles] ([RoleId])
    GO
    
    ALTER TABLE [dbo].[UserRoles]  
         WITH CHECK ADD  CONSTRAINT [FK_UserRoles_Users] FOREIGN KEY([UserId])
    REFERENCES [dbo].[Users] ([UserId])
    GO

并且,还会添加一个默认用户,用户名为administrator,密码为administrator。相应地,会在UserRoles表中为默认用户添加一条记录:该用户被添加为超级管理员,以便可以管理其他用户!

在引言中提供的起点zip文件已经包含了一个Setup.sql文件,其中包含创建Users表的SQL命令。如果您计划使用相同的数据库,请不要忘记在此脚本中注释掉创建Users表的代码部分。它还在Users表中包含了一个代表用户的User类。现在我将创建另外两个类,一个代表Role,一个代表UserRole,并修改User类以链接到分配给该用户的角色(请记住,此数据访问层正在使用Entity Framework)。它们如下所示。注意User中的UserRoles属性,可用于查找与用户关联的角色。还请注意UserRole类中的Role属性,可用于识别有关角色的更多信息。最后,[Key]是Entity Framework提供的属性,用于指示该属性是此表的[主键]。由于本文的范围不包括讨论Entity Framework,因此我在此省略。

public class User
{
    [Key]
    public int UserId { get; set; }
    public string UserName { get; set; }
    public string Password { get; set; }
    public string UserEmailAddress { get; set; }

    public virtual ICollection<UserRole> UserRoles { get; set; }
}

public class Role
{
    [Key]
    public short RoleId { get; set; }
    public string RoleName { get; set; }
    public string RoleDescription { get; set; }
}

public class UserRole
{
    [Key]
    public int UserRoleId { get; set; }
    public int UserId { get; set; }
    public short RoleId { get; set; }

    public virtual Role Role { get; set; }
}

下一个改变的文件是UsersContext,它充当我们的存储库类。此类有各种属性,可以帮助我们访问这三个表,还提供诸如AddUser之类的辅助方法。此类如下所示,讨论将在列表之后进行!

public class UsersContext : DbContext
{
    public DbSet<User> Users { get; set; }
    public DbSet<Role> Roles { get; set; }
    public DbSet<UserRole> UserRoles { get; set; }

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

    public void AddUserRole(UserRole userRole)
    {
        var roleEntry = UserRoles.SingleOrDefault(r => r.UserId == userRole.UserId);
        if (roleEntry != null)
        {
            UserRoles.Remove(roleEntry);
            SaveChanges();
        }
        UserRoles.Add(userRole);
        SaveChanges();
    }
}

我向此类添加了两个新属性:RolesUserRoles,它们是DbSet - 用于访问新表的属性。然后,我添加了另一个方法AddUserRole,可用于为用户添加角色条目。下一个类非常重要。我添加的下一个类包含有关当前登录用户的大量有用信息 - UserIdentity

public class UserIdentity : IIdentity, IPrincipal
{
    private readonly FormsAuthenticationTicket _ticket;

    public UserIdentity(FormsAuthenticationTicket ticket)
    {
        _ticket = ticket;
    }

    public string AuthenticationType
    {
        get { return "User"; }
    }

    public bool IsAuthenticated
    {
        get { return true; }
    }

    public string Name
    {
        get { return _ticket.Name; }
    }

    public string UserId
    {
        get { return _ticket.UserData; }
    }

    public bool IsInRole(string role)
    {
        return Roles.IsUserInRole(role);
    }

    public IIdentity Identity
    {
        get { return this; }
    }
}

上述类包含各种属性,如UserIdNameIsAuthenticated等。除了这些之外,另一个有趣的[方法]是IsInRole,我稍后会回来讨论它,但简单来说,如方法内容所示,它会调用Roles类中的IsUserInRole方法来查看用户是否属于某个角色并返回结果。如果注意到,此类构造函数会收到一个FormsAuthenticationTicket实例,这是您将在下一节中看到的Cookie的解密版本。稍后将详细介绍!

先决条件 - 编写登录操作

在本节中,我只是继续讲解先决条件的细节 :) 引言中讨论的下载文件在用户成功[身份验证]后[什么]都不做。但我现在不能再这样做了!因此,我将挂钩到PostAuthenticateRequest事件以进行一些自定义处理。以下部分向您展示了在此事件中执行的操作!

void MvcApplication_PostAuthenticateRequest(object sender, EventArgs e)
{
    var authCookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName];
    if (authCookie != null)
    {
        string encTicket = authCookie.Value;
        if (!String.IsNullOrEmpty(encTicket))
        {
            var ticket = FormsAuthentication.Decrypt(encTicket);
            var id = new UserIdentity(ticket);
            var prin = new GenericPrincipal(id, null);
            HttpContext.Current.User = prin;
        }
    }
}

在上图中,我获取身份验证Cookie,解密票证,并创建前面描述的UserIdentity类的实例。然后创建GenericPrincipal的实例,并将其存储在HttpContext.Current.User属性中。现在可以在整个应用程序中访问此属性!此事件在用户登录时成功[身份验证]时触发(如下所示)。在事件触发之前,会创建一个[身份验证]票证(第36行),将其加密(第37行),并将其存储在Cookie集合中(第38行)。请查看AccountController的以下列表

[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        if (MembershipService.ValidateUser(model.UserName, model.Password))
        {
            SetupFormsAuthTicket(model.UserName, model.RememberMe);
            // -- Snip --
            return RedirectToAction("Index", "Home");
        }
        ModelState.AddModelError("", 
          "The user name or password provided is incorrect.");
    }
    return View(model);
}

// -- Snip --

private User SetupFormsAuthTicket(string userName, bool persistanceFlag)
{
    User user;
    using (var usersContext = new UsersContext())
    {
        user = usersContext.GetUser(userName);
    }
    var userId = user.UserId;
    var userData = userId.ToString(CultureInfo.InvariantCulture);
    var authTicket = new FormsAuthenticationTicket(1, //version
                        userName, // user name
                        DateTime.Now,             //creation
                        DateTime.Now.AddMinutes(30), //Expiration
                        persistanceFlag, //Persistent
                        userData);

    var encTicket = FormsAuthentication.Encrypt(authTicket);
    Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encTicket));
    return user;
}

创建自定义角色提供程序

现在是真正的乐趣所在!在本节中,我将讨论实际的自定义角色提供程序!我的自定义角色提供程序将扩展ASP.NET MVC提供的RoleProvider类。尽管此类有许多方法,但在本文中,我将只关注其中的一些方法。因此,以下列表仅包含这些方法。

public class CustomRoleProvider : RoleProvider
{
    public override bool IsUserInRole(string username, string roleName)
    {
        using (var usersContext = new UsersContext())
        {
            var user = usersContext.Users.SingleOrDefault(u => u.UserName == username);
            if (user == null)
                return false;
            return user.UserRoles != null && user.UserRoles.Select(
                 u => u.Role).Any(r => r.RoleName == roleName);
        }
    }

    public override string[] GetRolesForUser(string username)
    {
        using (var usersContext = new UsersContext())
        {
            var user = usersContext.Users.SingleOrDefault(u => u.UserName == username);
            if (user == null)
                return new string[]{};
            return user.UserRoles == null ? new string[] { } : 
              user.UserRoles.Select(u => u.Role).Select(u => u.RoleName).ToArray();
        }
    }

    // -- Snip --

    public override string[] GetAllRoles()
    {
        using (var usersContext = new UsersContext())
        {
            return usersContext.Roles.Select(r => r.RoleName).ToArray();
        }
    }

    // -- Snip --
}

首先,我们来看IsUserInRole方法。在此方法中,我使用前面各节中描述的存储库(UsersContext)从数据库获取当前登录用户的引用。请注意,这发生在用户[身份验证]“之后”,因此不需要密码。如果此步骤失败,则返回false值,表示用户不属于此角色。如果我确实找到了用户,我首先查看导航属性UserRoles是否[有]任何内容[MSDN]。如果是,我使用UserRole中可用的导航属性来选择此用户的角色,并查看是否为该用户分配了与传递给方法的角色名(roleName)相同的角色。

接下来要讨论的方法是GetRolesForUser方法。在此方法中,我也首先检查是否可以找到具有[传入]用户名的条目。如果找到,我使用相同的导航属性来查找分配给用户的角色并返回它们。最后一个方法是GetAllRoles,我不认为您会希望我解释这个方法!

连接自定义角色提供程序

好的,我们现在已经编写了自定义角色提供程序。但是框架如何知道如何使用这个自定义角色提供程序呢?正如您所料,web.config负责这一切!在提供的下载文件中找到web.config文件中的roleManager部分。它看起来像这样

<roleManager enabled="false">
  <providers>
    <clear/>
    <add name="AspNetSqlRoleProvider" 
       type="System.Web.Security.SqlRoleProvider" 
       connectionStringName="ApplicationServices" applicationName="/" />
    <add name="AspNetWindowsTokenRoleProvider" 
       type="System.Web.Security.WindowsTokenRoleProvider" 
       applicationName="/" />
  </providers>
</roleManager>

上面的默认设置需要替换为以下设置,以便ASP.NET MVC知道我们自定义提供程序的完全限定名称,并且角色管理器已启用并包含[列表]中的提供程序。

<roleManager enabled="true" defaultProvider="CustomRoleProvider">
  <providers>
    <clear/>
    <add name="CustomRoleProvider" 
       type="CustomMembershipEF.Infrastructure.CustomRoleProvider, 
             CustomMembershipEF, Version=1.0.0.0, Culture=neutral" 
       connectionStringName="UsersContext"
       enablePasswordRetrieval="false" enablePasswordReset="true" 
       requiresQuestionAndAnswer="false" writeExceptionsToEventLog="false" />
  </providers>
</roleManager>

首先要注意的是,我将enabled属性设置为true,以便框架启用角色管理器。然后,您需要指定defaultProvider属性,该属性用于在指定了多个提供程序时[识别]默认提供程序。但在这种情况下,我只有一个提供程序CustomRoleProvider,仍然需要指定默认提供程序。这包含在providers元素中。clear元素用于清除应用程序先前存储的所有提供程序,例如默认提供程序。然后,我通过指定名称“CustomRoleProvider”(在defaultProvider属性中使用过)来定义自定义角色提供程序。这包含许多属性。最重要的是type属性,其中指定了自定义角色提供程序的完全限定名称(CustomMembershipEF.Infrastructure.CustomRoleProvider),后面是包含此类型的程序集(CustomMembershipEF)和版本。请注意,如果类型包含在与Web应用程序本身相同的程序集中,则只需要类型名称,其他选项都是可选的。其他属性不言自明,我不会在这里详细介绍!

在成功[身份验证]后提供用户的角色

好的,我们有了一个角色提供程序并将其[绑定]到了框架。接下来呢?在开始执行任何操作之前,我必须让框架了解[身份验证]用户的角色!这就是自定义角色提供程序的使用之处。让我们回到Global.asax.cs中的MvcApplication_PostAuthenticateRequest方法。如果您还记得,这是用户成功[身份验证]后触发的事件。之前,当我创建GenericPrincipal的实例时,我将用户的身份作为第一个参数,并将null作为第二个参数传递。第二个参数代表用户拥有的角色,它是一个字符串数组。在示例应用程序中,用户只能有一个角色。但这很容易更改。由于我们需要角色,因此我不能再传递null。因此,我使用自定义角色提供程序的GetRolesForUser方法来获取该用户的角色。更新后的事件处理程序如下所示

void MvcApplication_PostAuthenticateRequest(object sender, EventArgs e)
{
    var authCookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName];
    if (authCookie != null)
    {
        string encTicket = authCookie.Value;
        if (!String.IsNullOrEmpty(encTicket))
        {
            var ticket = FormsAuthentication.Decrypt(encTicket);
            var id = new UserIdentity(ticket);
            var userRoles = Roles.GetRolesForUser(id.Name);
            var prin = new GenericPrincipal(id, userRoles);
            HttpContext.Current.User = prin;
        }
    }
}

注意第11行和第12行。在第11行,我调用Roles静态类中的GetRolesForProvider。然后,在创建GenericPrincipal实例时,将用户的角色传递给GenericPrincipal构造函数。但是等等,我知道您可能想知道Roles类是从哪里来的。这不过是我们自定义角色提供程序CustomMembershipEF.Infrastructure.CustomRoleProvider的一个实例的引用,该实例是由框架为我们创建的,因为我们在web.config文件中指定了它作为我们的自定义角色提供程序!请注意,您无需更改一行代码,如果您想创建一个新的角色提供程序,只需在web.config中更改即可!

关于Authorize属性的几点说明

在开始使用角色提供程序之前,我认为有必要稍微讨论一下Authorize属性(第一部分和第二部分对此有更详细的讨论)。当一个操作方法被Authorize属性[装饰]时,只有登录用户才能访问该操作方法。如果一个未[身份验证]的用户尝试访问该操作方法,他/她将被重定向到登录页面。在下面的方法中,Protected方法被Authorize属性[装饰]。一旦用户[身份验证]成功,如果您还记得对Global.asax.cs文件的MvcApplication_PostAuthenticateRequest所做的更改,我将UserIdentity对象存储在HttpContext.Current.User中。由于用户已[身份验证],因此可以使用此属性访问用户特定属性。

[Authorize]
public ActionResult Protected()
{
    var user = (UserIdentity) User.Identity;
    return View((object)user.UserId);
}

如果您注意到,在第3行,HttpContext.Current.User被隐式地访问为UserUserIdentity属性存储在user中。现在,为了向您展示如何使用其中一个属性,我将当前登录用户的UserId作为模型传递给视图。

使用自定义角色提供程序

我知道您可能在想这个部分会有什么内容,因为我已经使用了自定义角色提供程序。但这仅仅是开始!这个自定义角色提供程序有很多用途,让我们来看几个!这是角色提供程序的第一种[示例]用法!

[Authorize(Roles = "SuperAdmin")]
public ActionResult SuperAdmin()
{
    return View();
}

Authorize属性接受一个名为Roles的参数,我用它来设置允许访问此操作方法的角色。在这种情况下是“SuperAdmin”角色。因此,如果一个未登录的用户,或者一个已登录但[不]属于此角色的用户尝试访问此操作方法,他们将被重定向到登录页面!

早些时候,如果您还记得,我在“在成功[身份验证]后提供用户的角色”部分讨论了设置GenericPrincipal类。因此,当一个操作方法被[指定]了所需角色,或者当调用User.IsInRole方法时,框架会使用传递的角色来决定用户是否可以访问相应的操作方法。

您也可以传递一个逗号(,)分隔的角色列表,以便为此操作方法提供对多个角色的访问。还有另一种[方法]可以用来在用户访问该操作方法之前[检查]用户是否属于某个角色。如下所示

[Authorize]
public ActionResult AdminOrSuperAdmin()
{
    if (!User.IsInRole("SuperAdmin") && !User.IsInRole("Admin"))
    {
        return RedirectToAction("Index", "Home");
    }
    return View();
}

在上面的示例中,我使用User对象提供的IsInRole方法,而不是使用Roles参数。如果您还记得,在UserIdentity类中,我实现了IPrincipal接口。该接口强制类实现IsInRole方法和ApplicationName属性。在IsInRole方法中,我使用Roles类来获取用户的角色并[验证]用户是否拥有此角色(前面已解释)。如第3行所示,如果用户不属于“SuperAdmin”或“Admin”中的任何一个,我会将用户重定向到主页(与使用Roles参数时的登录页面不同)。同样的效果也可以通过以下方式实现

[Authorize(Roles = "Admin, Author")]
public ActionResult AdminOrAuthor()
{
    return View();
}

根据用户角色选择性地显示链接

检查用户是否属于某个角色的各种方法也可以在视图中使用!例如,在我的布局中,我根据用户的角色选择性地显示链接。这是其中一部分

<div id="menucontainer">
    <ul id="menu">
        <li>@Html.ActionLink("Home", "Index", "Home")</li>
        <li>@Html.ActionLink("About", "About", "Home")</li>
        <li>@Html.ActionLink("Protected", "Protected", "Home")</li>
        @if (User.IsInRole("SuperAdmin"))
        {
            <li>@Html.ActionLink("Super Admin", 
              "SuperAdmin", "Home")</li>
        }
        @if (User.IsInRole("Admin"))
        {
            <li>@Html.ActionLink("Admin", "Admin", "Home")</li>
        }
        @if (User.IsInRole("SuperAdmin") || User.IsInRole("Admin"))
        {
            <li>@Html.ActionLink("Admin Or Super Admin", 
               "AdminOrSuperAdmin", "Home")</li>
        }
        @if (User.IsInRole("Admin") || User.IsInRole("Author"))
        {
            <li>@Html.ActionLink("Admin Or Author", 
              "AdminOrAuthor", "Home")</li>
        }
        @if (User.IsInRole("SuperAdmin"))
        {
            <li>@Html.ActionLink("Manage Users", 
              "Index","Manage")</li>
        }
    </ul>
</div>

从上面的列表可以明显看出,就像AdminOrSuperAdmin操作方法一样,我使用User对象的IsInRole方法来[查找]当前登录用户是否属于某个角色。因此,在布局中,链接会根据角色显示,以[对应]控制器中采取的操作。

更新用户的角色

我还添加了一个页面,可以用来更新用户的角色。此页面的工作原理很简单:除了id为1的用户外,所有用户都列在此页面上。每个用户都有一个相应的下拉列表,可以使用该列表为作者设置角色。从表结构来看,我想您会意识到一个用户可以有多个角色。但为了简化起见,我假设一个用户只能有一个角色。下面是此页面的[屏幕截图]。

Sample Image - maximum width is 600 pixels

此页面[工作]的方式[简单]而直接。当用户[对应]的下拉列表更改(或未更改)并且单击“设置”按钮时,会向服务器发出[AJAX]请求,并将更新用户的角色。由于这超出了本文的范围,因此我将在此[打住]!

后续步骤

关于角色可以进行许多[改进]。例如,可以有一个Rights表,该表映射到Roles表中的一个角色。用户管理能力,例如,是一个“权利”。因此,Rights表中会有一个条目。使用这种方法,可以[扩展]UserIdentity类以[检查]用户是否可以执行某个功能,或者也可以在操作方法内部[实现]相同的功能。因此,有很多[途径]可以[改进]此项目,也许在某个时候,我将根据这个想法发布一个对本文的更新。

历史

  • 本文第1版发布。
© . All rights reserved.