自定义角色提供程序






4.97/5 (32投票s)
介绍在ASP.NET MVC 3应用程序中使用Entity Framework进行ORM的自定义角色提供程序。
- 自定义角色提供程序的GitHub存储库 (以便您获得最新版本!)
引言
就像身份验证对于Web应用程序至关重要一样,角色也出于多种原因而必不可少。例如,角色可以用来限制应用程序的某些功能的可用性,仅限于特定用户组。本文完全基于ASP.NET MVC提供的RoleProvider
抽象类(MSDN)。阅读完本文后,您将能够使用文章第一部分提供的示例应用程序(下载)并根据三个角色进行页面划分 - “超级管理员”,“管理员”和“作者”。
背景
我之前写过一篇关于使用自定义成员资格提供程序的文章,并收到了很多评论。其中一条评论是跟进一篇关于自定义角色提供程序的文章。本文可被视为我之前关于自定义成员资格提供程序的文章的“续篇”!我使用了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();
}
}
我向此类添加了两个新属性:Roles
和UserRoles
,它们是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; }
}
}
上述类包含各种属性,如UserId
,Name
,IsAuthenticated
等。除了这些之外,另一个有趣的[方法]是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
被隐式地访问为User
。User
的Identity
属性存储在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的用户外,所有用户都列在此页面上。每个用户都有一个相应的下拉列表,可以使用该列表为作者设置角色。从表结构来看,我想您会意识到一个用户可以有多个角色。但为了简化起见,我假设一个用户只能有一个角色。下面是此页面的[屏幕截图]。
此页面[工作]的方式[简单]而直接。当用户[对应]的下拉列表更改(或未更改)并且单击“设置”按钮时,会向服务器发出[AJAX]请求,并将更新用户的角色。由于这超出了本文的范围,因此我将在此[打住]!
后续步骤
关于角色可以进行许多[改进]。例如,可以有一个Rights
表,该表映射到Roles
表中的一个角色。用户管理能力,例如,是一个“权利”。因此,Rights
表中会有一个条目。使用这种方法,可以[扩展]UserIdentity
类以[检查]用户是否可以执行某个功能,或者也可以在操作方法内部[实现]相同的功能。因此,有很多[途径]可以[改进]此项目,也许在某个时候,我将根据这个想法发布一个对本文的更新。
历史
- 本文第1版发布。