ASP.NET MVC 应用程序中的自定义基于角色的访问控制 (RBAC) - 第一部分 (框架介绍)






4.95/5 (160投票s)
介绍使用 Entity Framework 在 ASP.NET MVC 应用程序中实现自定义的基于角色的访问控制。
引言
在本篇文章中,我将介绍在基于 Windows 身份验证的 ASP.NET MVC 内网应用程序中实现自定义的基于角色的访问控制 (RBAC) 以及后续的角色维护。ASP.NET 的角色和成员资格提供了几乎所有进行身份验证和授权所需的功能,但添加新角色并将其分配给特定用户似乎已被遗忘。此解决方案形成一个独立的框架,独立于默认的开箱即用提供程序。该框架允许我们专注于应用程序中哪些功能/区域对用户有限制,包括菜单,以及哪些信息对用户可见/不可见,而无需关心底层技术细节。该框架在控制器操作和控制器视图的细粒度级别上提供 RBAC 功能,同时使用最少的代码语法,并且可以扩展该框架以包含自定义 RBAC 方法。它特别适用于公司内网应用程序,在这些应用程序部署后,对托管 Web 服务器的访问受到限制,或者应用程序的系统管理员或所有者无法直接进行用户角色(包括角色分配)的管理。
背景
多年来,在组织中使用 Windows 身份验证开发内网应用程序一直很普遍。在大多数组织中,通常内网应用程序不仅需要允许访问由组织 Active Directory 服务器定义的子集用户,还需要定义分配给应用程序用户的角色,从而限制对应用程序内特定功能/区域的访问。同样,基于角色的访问控制并不是一个新概念,并且有许多已发布的示例以某种形式证明了这一概念。ASP.NET MVC 非常适合 RBAC,而网络上发布的各种形式的示例要么过度设计了这个概念,要么过于简单化而忽略了可扩展性。正是出于这个原因,我写了这篇文章。
使用代码
当我们想要在应用程序中提供身份验证和授权时,默认的 ASP.NET 角色和成员资格类非常方便。这种方法要求您预先定义角色,然后在 MVC 应用程序中通过函数属性引用这些角色;本质上是硬编码值。
[Authorize(Roles="Administrators")]
public class AdminController : Controller
{
. . .
}
这种方法的几个问题立刻显现出来……
- 在不重新编译和重新部署我的应用程序的情况下,如何在应用程序部署后创建新的自定义角色并将它们动态绑定到控制器方法?
- 如何动态地将用户关联到多个角色,其中最高的应用程序权限优先?
- 如何根据请求用户的角色及其关联的应用程序权限动态控制菜单或控制器视图的呈现?
在开发公司解决方案时,我们通常会遇到应用程序用户的角色数据需要存储在应用程序自己的数据库中的情况。如果数据库服务器因硬件故障而发生故障,恢复应用程序数据库的早期备份副本将包含所有角色数据,从而有利于数据库复制,用于热备数据库服务器。
自定义控制器/操作授权
选择实现我们自己的自定义身份验证/授权机制将意味着放弃默认的 ASP.NET 角色和成员资格身份验证/授权机制。然而,这样做可以更精细地控制用户角色和应用程序权限。总的来说,一旦部署,应用程序就应该通过应用程序的系统管理员进行自我维护和管理;当应用程序开发人员本质上成为该应用程序的用户/角色管理员时,这是一项耗时且昂贵的工作,除非目的是利用支持团队维护后端角色的依赖关系。
授权
基于角色的应用程序是指用户被分配了特定角色的应用程序。在我们的系统中,每个角色决定了角色可以通过应用程序权限访问应用程序的哪些区域。应用程序权限定义 MVC 控制器名称和控制器操作名称,表示为两个属性的字符串连接,格式为 controller-action(例如,“admin-index”)。应用程序权限是唯一的,可以追溯到其控制器-操作引用。很容易混淆用户身份验证和用户授权之间的区别。总之,身份验证是通过某种登录机制(用户名/密码、Windows 身份验证等——表示“这是谁”)来验证用户是否是他们所声称的那个人。授权是验证他们在您的网站上是否可以执行与其工作角色相关的任务。这通常通过某种基于角色的系统来实现。
基于角色的访问控制 (RBAC)
基于角色的访问控制是一种限制系统访问授权用户的方法。此机制可用于保护用户免于访问他们不需要的系统部分。它还可用于限制访问他们不需要查看的数据。
为各种工作职能创建角色,并且在应用程序部署很久之后,新角色被引入基于角色的系统并不少见。执行特定操作的权限被分配给特定角色。用户被分配了特定的角色,通过这些角色分配获得执行特定计算机系统功能的应用程序权限。由于用户不直接获得权限,而是仅通过其角色(或角色)获得权限,因此对单个用户权限的管理就变成了一个简单地将适当的角色分配给用户帐户的问题。系统中的每个用户可以被分配零个、一个或多个角色,具体取决于他们在业务流程中的职责。
准备数据库
为了作为我们身份验证/授权框架的基础,我们需要在应用程序现有的数据库中添加几个表(如果当前存在),或者创建一个包含这些表的新应用程序数据库。这些表源自以下实体关系 (ER) 图。
RBAC 实体关系图
我们的实体关系图表明,应用程序用户可以被分配零个或多个应用程序角色。一个应用程序角色可以被分配零个或多个应用程序权限。应用程序权限代表控制器操作方法。
随后,我们从实体关系图中派生出以下数据库表。
在我们的实际应用程序中,我们会清楚地使用更多的表属性来实现灵活的定制,但所演示的表提供了形成 RBAC 框架基础所需的最少属性。将我们的自定义身份验证/授权机制集成到现有的 MVC 应用程序中应该相对直接,因为不需要“额外”的数据库或身份管理提供程序 (IdM)。任何希望引入 RBAC 的 MVC 应用程序在任何情况下都可能在没有后端数据库的情况下运行,因此我们可以简单地将上述表添加到现有数据库中。然而,我们也可以选择将 RBAC 表与主应用程序数据库分开,因为我们的 RBAC 表是独立的,并且基于松耦合设计。任何没有后端数据库的 MVC 应用程序通常都不需要 RBAC,例如单位/货币转换网站。
从我们的 RBAC 表中检索用户应用程序权限
以下 SQL 将从我们的数据库表中检索用户的应用程序权限,此处仅用于说明目的。
SELECT Permission_Id, PermissionDescription
FROM PERMISSIONS
WHERE Permission_Id IN (
SELECT DISTINCT(Permission_Id)
FROM LNK_ROLE_PERMISSION
WHERE Role_Id IN (
SELECT DISTINCT(Role_Id)
FROM LNK_USER_ROLE ur
JOIN USERS u ON u.User_Id=ur.User_Id
WHERE u.Username='swloch'))
用户角色/权限类映射
现在,让我们使用一个类来表示用户关联的角色和应用程序权限,该类封装了必要的辅助方法来检查用户的角色及其关联的权限。我们将在稍后更详细地介绍 `GetDatabaseUserRolesPermissions()` 方法。此类将在我们的整个应用程序中使用,以确定用户授权。
注意:本文中提供的代码片段是极简的,仅用于说明目的,以便专注于当前主题。可下载的示例项目扩展了演示的代码。
public class RBACUser
{
public int User_Id { get; set; }
public bool IsSysAdmin { get; set; }
public string Username { get; set; }
private List<UserRole> Roles = new List<UserRole>();
public RBACUser(string _username)
{
this.Username = _username;
this.IsSysAdmin = false;
GetDatabaseUserRolesPermissions();
}
private void GetDatabaseUserRolesPermissions()
{
//Get user roles and permissions from database tables...
}
public bool HasPermission(string requiredPermission)
{
bool bFound = false;
foreach (UserRole role in this.Roles)
{
bFound = (role.Permissions.Where(
p => p.PermissionDescription == requiredPermission).ToList().Count > 0);
if (bFound)
break;
}
return bFound;
}
public bool HasRole(string role)
{
return (Roles.Where(p => p.RoleName == role).ToList().Count > 0);
}
public bool HasRoles(string roles)
{
bool bFound = false;
string[] _roles = roles.ToLower().Split(';');
foreach (UserRole role in this.Roles)
{
try
{
bFound = _roles.Contains(role.RoleName.ToLower());
if (bFound)
return bFound;
}
catch (Exception)
{
}
}
return bFound;
}
}
public class UserRole
{
public int Role_Id { get; set; }
public string RoleName { get; set; }
public List<RolePermission> Permissions = new List<RolePermission>();
}
public class RolePermission
{
public int Permission_Id { get; set; }
public string PermissionDescription { get; set; }
}
`RBACUser` 类封装了自定义用户身份验证/授权功能,并将执行在“操作过滤器”中,该过滤器支持对控制器操作方法的预操作行为。操作过滤器将在下一节中进行解释。
MVC 控制器操作方法的基本概述
MVC 控制器负责响应针对 ASP.NET MVC 网站发出的请求。MVC 尝试将每个浏览器请求映射到一个特定的控制器操作。如果在 URL 中未指定控制器操作,则使用默认控制器操作“index”。如果基础控制器或控制器操作不存在,则会向浏览器返回 HTTP 404 错误。例如,在 Web 浏览器中输入以下 URL 将导致 MVC 尝试调用“Admin”控制器中的“Index”操作。
https:///Admin
URL 中的“Admin”动词因其在 URL 路径中的位置而表示控制器名称。在此示例中,将调用 `AdminController` 类;MVC 的控制器类命名约定是在控制器名称后附加关键字“Controller”。MVC 控制器类负责处理和响应浏览器请求。每个控制器类都应公开通过 URL 引用或其他路径调用的控制器操作方法。
以下 URL 同时指定了控制器名称和控制器操作方法。
https:///Admin/Create
MVC 将尝试调用 `AdminController` 类中的“Create”控制器操作方法。每个控制器操作都会响应浏览器请求返回操作结果,即使引用的控制器或控制器操作不存在。在执行调用到的控制器操作方法之前,可以使用“操作过滤器”指示预处理,其中可以放置逻辑来确定是否应执行操作方法或将其重定向到系统的其他部分。“操作过滤器”是检查用户授权以进行调用操作的理想选择。
角色与 MVC 控制器/操作关联
现在我们对控制器操作方法的调用方式有了基本了解,并且 MVC 提供了一种在操作方法执行之前处理逻辑的方法,以评估调用操作方法是否应被执行,我们需要理解控制器操作方法与我们的应用程序角色之间的关联。在我们的系统中,每个角色将定义多个应用程序“控制器-操作”关联;回想一下,应用程序权限定义了 MVC 控制器名称和控制器操作名称,格式为“controller-action”。
让我们看一个例子;我们的应用程序中定义的“Administrator”角色必须能够通过“Admin”控制器使用“Create”操作方法来创建新用户。因此,我们需要将应用程序权限“admin-create”与“Administrator”角色关联。同样,“Standard User”角色不应具有此能力,因此不得具有应用程序权限关联。我们的示例创建用户操作方法只是将一个包含文本字段和提交按钮的页面返回到浏览器,管理员可以使用该页面在后端数据库中创建新用户。显然,我们不希望任何人都能访问此页面。
我们需要创建应用程序权限“admin-create”并将该权限分配给我们的“Administrator”角色,因为控制器名称和操作方法名称的组合将作为所需的应用程序权限传递给我们的“授权”逻辑,以评估请求用户的允许应用程序权限。您可以根据需要更改数据库中存储的应用程序权限的格式,但它需要在整个应用程序中保持一致。
目前,我们将手动创建并将应用程序权限分配到数据库中,以了解我们的 RBAC 表的结构,但可下载的示例项目提供了一个管理菜单,使系统管理员能够动态地对用户/角色/权限执行 CRUD 操作。
--Create an 'Administrator' Role setting IsSystemRole=1
INSERT INTO ROLES(RoleName, IsSysAdmin) VALUES('Administrator', 1)
--Create a 'Standard User' Role setting IsSystemRole=0
INSERT INTO ROLES(RoleName, IsSysAdmin) VALUES('Standard User', 0)
--Create an Application Permission for the action method 'Create'
--defined in the 'Admin' controller (ie 'admin-create')
INSERT INTO PERMISSIONS(PermissionDescription) VALUES('admin-create')
--Associate the Application Permission 'admin-create' with the 'Administrator' Role
INSERT INTO LNK_ROLE_PERMISSION VALUES(
(SELECT Role_Id FROM ROLES WHERE RoleName = 'Administrator'),
(SELECT Permission_Id FROM PERMISSIONS WHERE PermissionDescription = 'admin-create'))
上面的表显示了与 SQL 'INSERT
' 命令相关的 'ROLES'、'PERMISSIONS' 和 'LNK_ROLE_PERMISSION' 表的内容视图。
现在我们已经定义了一个应用程序角色并向新角色分配了一个应用程序权限,我们需要创建一个分配了“Administrator”角色的应用程序用户。为了简单起见,我们将假设我们的应用程序是一个使用 IIS 集成 Windows 身份验证的内网系统,其中请求资源的用户的身份已得到验证。我们只需使用 `IPrincipal` 对象来识别请求用户的姓名,然后我们将他们的唯一用户名映射到我们的 **USERS** 表。
不用说,只有被允许访问内网站点的用户才会被添加到 **USERS** 表中,除非您采用用户“注册”机制,在这种机制下,用户将被分配“standard”用户角色。这对于不基于集成 Windows 身份验证的网站也同样适用,在这些网站中,身份验证是通过身份验证令牌实现的和跟踪的,这将在下一篇文章中讨论。在任何一种情况下,用户都通过 `IPrincipal` 对象进行标识。
假设我们将要分配应用程序管理角色的用户的 Windows 用户名是“swloch”,并且该用户的 `IPrincipal.Identity.Name` 属性评估为“somedomain\swloch”。
我们需要将用户添加到 USERS 表中,使用 Windows 用户名(不含前缀域)作为表 Username 字段,并将“Administrator”角色分配给用户。
--Create the user 'swloch'
INSERT INTO USERS(Username) VALUES('swloch')
--Associate the 'Administrator' Role with user
INSERT INTO LNK_USER_ROLE VALUES(
(SELECT User_Id FROM USERS WHERE Username = 'swloch'),
(SELECT Role_Id FROM ROLES WHERE RoleName = 'Administrator'))
上面的表显示了与上述 SQL 'INSERT
' 命令相关的 'USERS' 和 'LNK_USER_ROLE' 表的内容视图。
在控制器操作执行之前,将确定请求用户的角色,以检查该角色是否包含请求的控制器/操作组合。如果角色确实包含控制器/操作关联,则允许执行控制器操作,从而返回该控制器/操作的操作结果页面。如果角色不包含所需的控制器/操作关联,则返回一个无效的授权页面,而不是操作结果页面。
操作过滤器
在 MVC 中,控制器定义的操作方法通常与可能的\}, 用户交互(如单击链接或提交表单)有一对一的关系。偶尔,您希望在调用操作方法之前或之后执行逻辑。为了支持这一点,MVC 提供了操作过滤器。操作过滤器是自定义属性,提供了一种声明式的方法来为控制器操作方法添加预操作和后操作行为。
ASP.NET MVC 提供以下类型的操作过滤器:
- 授权过滤器,它做出关于是否执行操作方法的安全决策,例如执行身份验证或验证请求的属性。`AuthorizeAttribute` 类就是一个授权过滤器的例子。
- 操作过滤器,它封装操作方法的执行。此过滤器可以执行额外的处理,例如为操作方法提供额外的数据,检查返回值,或取消操作方法的执行。
- 结果过滤器,它封装 `ActionResult` 对象的执行。此过滤器可以对结果执行额外的处理,例如修改 HTTP 响应。`OutputCacheAttribute` 类就是一个结果过滤器的例子。
- 异常过滤器,当操作方法中出现未处理的异常时执行,从授权过滤器开始,到结果执行结束。异常过滤器可用于日志记录或显示错误页面等任务。`HandleErrorAttribute` 类就是一个异常过滤器的例子。
自定义操作过滤器
让我们创建一个名为 `RBACAttribute` 的自定义授权过滤器,它继承自 `AuthorizeAttribute` 类。我们的自定义属性从 `AuthorizationContext` 对象中提取请求的控制器名称和控制器操作名称属性,并使用 `RBACUser` 对象检查派生的应用程序权限是否存在于请求用户的任何已分配角色中作为允许的权限。
public class RBACAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
/*Create permission string based on the requested controller
name and action name in the format 'controllername-action'*/
string requiredPermission = String.Format("{0}-{1}",
filterContext.ActionDescriptor.ControllerDescriptor.ControllerName,
filterContext.ActionDescriptor.ActionName);
/*Create an instance of our custom user authorisation object passing requesting
user's 'Windows Username' into constructor*/
RBACUser requestingUser = new RBACUser(filterContext.RequestContext
.HttpContext.User.Identity.Name);
//Check if the requesting user has the permission to run the controller's action
if (!requestingUser.HasPermission(requiredPermission) & !requestingUser.IsSystemAdmin)
{
/*User doesn't have the required permission and is not a SysAdmin, return our
custom '401 Unauthorized' access error. Since we are setting
filterContext.Result to contain an ActionResult page, the controller's
action will not be run.
The custom '401 Unauthorized' access error will be returned to the
browser in response to the initial request.*/
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary {
{ "action", "Index" },
{ "controller", "Unauthorised" } });
}
/*If the user has the permission to run the controller's action, then
filterContext.Result will be uninitialized and executing the controller's
action is dependant on whether filterContext.Result is uninitialized.*/
}
}
`RBACUser` 对象公开 `HasPermission` 方法,该方法接受一个权限参数,并返回一个布尔值,表示该权限是否存在于用户的任何已分配角色中。如果您从 `AuthorizeAttribute` 类派生一个类,则派生类必须是线程安全的。因此,除非状态信息旨在应用于所有请求,否则不要在类的实例字段中存储状态信息。而是将状态信息按请求存储在 `Items` 属性中,该属性可以通过传递给 `AuthorizeAttribute` 的上下文对象访问。
如果用户的角色不包含所需的应用程序权限,则会返回一个自定义的“**401 Unauthorized**”访问错误,而不是预期的控制器操作结果视图。自定义错误通过“**Unauthorised**”控制器中定义的“Index”操作返回一个视图,该操作由 `RedirectToRouteResult` 调用。
错误文本定义在“**Unauthorised**”控制器对应的“Index.cshtml”文件中,您应该修改此文件以更改页面的外观。
UnauthorisedController.cs
public class UnauthorisedController : Controller
{
// GET: Unauthorised
public ActionResult Index()
{
Session.Abandon();
return View();
}
}
与“**Unauthorised**”控制器对应的 Index.cshtml
<body>
@{
ViewBag.Title = "Unauthorised Request";
}
<div id="title">Error 401 : Unauthorised Request</div>
<div id="error">You do not have permission to access the requested resource due to security restrictions. In order to gain access, please speak to your system administrator.</div>
</body>
使用操作过滤器限制对 MVC 控制器操作方法的访问
我们现在可以使用自定义授权过滤器来限制对我们 Web 应用程序中控制器操作方法的访问,只需在操作方法上装饰我们的 `RBACAttribute` 属性;MVC 允许您在装饰控制器操作时省略 `authorization` 属性的 `Attribute` 动词,以便那些喜欢这种命名约定的人可以使用 `RBAC`。我们的 `authorization` 属性指示 MVC 在调用操作方法之前执行逻辑,其中逻辑检查请求用户是否已通过身份验证并且是否具有执行控制器操作方法所需的应用程序权限。
[RBAC]
public ActionResult Create()
{
return View();
}
OR
[RBACAttribute]
public ActionResult Create()
{
return View();
}
安全的良好方法是始终将安全检查放在要保护的资源附近。您可能在堆栈的更高层进行其他检查,但最终,您需要保护实际资源。这样,无论用户如何到达资源,总会有一个安全检查到位。在这种情况下,您不希望依赖路由和 URL 授权来保护控制器;您真的需要保护控制器本身。
我们的自定义 `RBACAttribute` 授权属性实现了这一目的。
- 如果您不指定任何角色或用户,则当前用户只需经过身份验证即可调用操作方法。这是阻止未经身份验证的用户访问特定控制器操作的一种简单方法。
- 如果用户尝试访问应用了此属性的操作方法但未通过授权检查,则过滤器会导致服务器返回“**401 Unauthorised**”HTTP 状态代码。
在控制器操作执行期间使用 RBAC 进行条件处理
让我们考虑一个控制器操作方法,它返回一个包含员工数据的页面。这次,我们没有在控制器的操作上装饰我们的自定义 `RBACAttribute` 属性,因为我们应用程序中所有注册的用户都可以访问此页面。但是,我们确实希望根据请求用户的角色限制控制器操作在执行期间返回的数据。
例如,具有“Standard”用户角色的用户可以访问控制器操作(控制器操作未应用授权过滤器),但只能查看员工的工作相关联系方式,而具有“HumanResourcesManager”角色的用户可以查看其他信息,包括员工的薪资。因此,我们需要在控制器操作方法和相应的视图中检查请求用户的角色,以确定返回到浏览器的员工数据的级别。
由于我们的 `RBACUser` 类封装了评估用户角色/权限所需的函数,因此我们需要将此函数暴露给控制器操作方法和视图。最简单的方法是通过“**扩展方法**”将我们的自定义函数暴露给 `System.Web.Mvc.ControllerBase` 抽象类。这使得我们的用户角色/权限功能可供每个控制器在执行期间使用,从而将代码更改降至最低,即使您有多个控制器和相应的视图,因为无需更改每个控制器为一个新控制器类,该类继承自 `Controller` 并公开我们的函数。
让我们扩展 `System.Web.Mvc.ControllerBase` 类以公开我们 `RBACUser` 所需的函数,用于角色/权限管理。
public static class RBAC_ExtendedMethods
{
public static bool HasRole(this ControllerBase controller, string role)
{
bool Found = false;
try
{
//Check if the requesting user has the specified role...
Found = new RBACUser(controller.ControllerContext
.HttpContext.User.Identity.Name).HasRole(role);
}
catch { }
return Found;
}
public static bool HasRoles(this ControllerBase controller, string roles)
{
bool bFound = false;
try
{
//Check if the requesting user has any of the specified roles...
//Make sure you separate the roles using ';' (ie "Sales Manager;Sales Operator")
bFound = new RBACUser(controller.ControllerContext
.HttpContext.User.Identity.Name).HasRoles(roles);
}
catch { }
return bFound;
}
public static bool HasPermission(this ControllerBase controller, string permission)
{
bool Found = false;
try
{
//Check if the requesting user has the specified application permission...
Found = new RBACUser(controller.ControllerContext
.HttpContext.User.Identity.Name).HasPermission(permission);
}
catch { }
return Found;
}
public static bool IsSysAdmin(this ControllerBase controller)
{
bool IsSysAdmin = false;
try
{
//Check if the requesting user has the System Administrator privilege...
IsSysAdmin = new RBACUser(controller.ControllerContext
.HttpContext.User.Identity.Name).IsSysAdmin;
}
catch { }
return IsSysAdmin;
}
}
现在,我们可以通过控制器的上下文对象在任何控制器操作和/或相应的视图中调用我们暴露的函数,如下例所示。
控制器操作 (EmployeeController.cs)
通过我们的 `RBAC_ExtendedMethods` 类暴露的 `RBACUser` 函数可以在控制器操作中使用。
控制器操作视图 (Index.cshtml)
通过我们的 `RBAC_ExtendedMethods` 类暴露的 `RBACUser` 函数可以在视图中使用。
在控制器操作方法中使用 RBAC
以下列表说明了通过控制器上下文对象在控制器操作中使用我们的自定义“HasRole”和“HasPermission”方法(在 `RBACUser` 类中公开)。我们已通过 `RBAC_ExtendedMethods` 类中定义的扩展方法将这些方法扩展到了控制器的上下文对象。
public class EmployeeController : Controller
{
// GET: Employee
public ActionResult Index()
{
if (this.HasRole("HumanResourcesManager"))
{
/*This code block is permitted as the requesting user has the 'HumanResourcesManager'
role assigned. Perform additional tasks and/or extract additional data from the
database into the controller's view model/viewbag in order to be passed down to the
controller's view.*/
}
if (this.HasPermission("ViewRestrictedHRData"))
{
/*This code block is permitted as the requesting user has the 'ViewRestrictedHRData'
permission assigned. We can also define role functionality permissions not related
to controller-action access. Extract salary data from database into controller's
view model/viewbag...*/
}
return View();
}
}
无需修改 `EmployeeController` 类,我们现在就可以通过控制器上下文对象使用扩展方法访问我们的 `RBACUser` 函数。这对控制器的视图也是如此。
在控制器视图中使用 RBAC
以下列表说明了我们自定义的“HasRole”和“HasPermission”方法的使用。我们已通过 `RBAC_ExtendedMethods` 类中定义的扩展方法将这些方法扩展到了控制器的上下文对象。
<body>
@{
ViewBag.Title = "Employee Page";
}
@{
if (ViewContext.Controller.HasRole("HumanResourcesManager"))
{
<div id="manager">Use this area to provide additional information and/or display
additional data provided in the model/viewbag by the controller action
as the user has the "HumanResourcesManager" role assigned.</div>
}
if (ViewContext.Controller.HasPermission("ViewRestrictedHRData"))
{
<div id="restricted">Use this area to provide additional information and/or display
additional data provided in the model/viewbag by the controller action
as the user has the "ViewRestrictedHRData" permission assigned.</div>
}
}
<div id="standard">Use this area to provide standard information to all users.</div>
</body>
我们的 `RBAC_ExtendedMethods` 类为我们提供了一个灵活的框架,使我们能够轻松地扩展我们的自定义 RBAC 函数。通过控制器上下文对象,我们的扩展 RBAC 方法会自动暴露给我们应用程序中的每个控制器操作和相应的视图,而无需以任何方式更改控制器类(除了使用我们新暴露的函数)。
使用 RBAC 动态控制菜单
我们的自定义“HasPermission”、“HasRole”和“IsSysAdmin”方法在显示动态菜单项时非常有用。回想一下,我们系统中的每个角色都将定义多个应用程序“控制器-操作”关联,每个关联代表一个控制器的名称和控制器操作名称。请考虑下面显示的应用程序菜单项。
如果我们想根据请求用户的角色权限动态显示菜单项,我们可以简单地将菜单的目标控制器名称和控制器操作作为格式为“controller-action”的权限来引用。
例如,如果我们希望“Import Data”菜单仅对允许访问底层控制器操作的用户可见,我们只需将我们的自定义“HasPermission”方法包装在菜单项定义(通常在‘_Layout.cshtml’视图中)周围,如下所示,将菜单的目标控制器名称和控制器操作作为要检查的权限传递。
<body>
<div class="page">
...
<div id="menucontainer">
<ul>
@{
if (ViewContext.Controller.IsSysAdmin())
{
<li>
<a href="#" class="arrow">System Administration</a>
...
</li>
}
}
@{
if (ViewContext.Controller.HasPermission("data-import"))
{
<li>@Html.ActionLink("Import Data", "Import", "Data")</li>
}
}
<li>@Html.ActionLink("About", "About", "Home")</li>
<li>@Html.ActionLink("Contact", "Contact", "Home")</li>
<li>@Html.ActionLink("Home", "Index", "Home")</li>
</ul>
</div>
</div>
...
</body>
自定义“HasPermission”方法将检查请求用户的角色是否具有“data-import”权限,并相应地显示。此外,我们还可以使用“IsSysAdmin”方法来检查请求用户是否拥有 `IsSysAdmin` 数据库属性启用的角色。因此,没有“System Administrator”角色也没有定义“data-import”权限的角色,将看到以下菜单项(基于演示的代码片段),而不是上面显示的菜单项。
但是,如果用户直接输入 URL(例如‘http://…/Data/Import’)并且没有所需的权限,那么将返回一个自定义的“**401 Unauthorised**”访问错误,而不是预期的控制器操作结果视图,前提是我们已经用我们的 `RBACAttribute` 装饰了控制器操作或控制器类。将“data-import”权限与用户角色关联将自动显示相应的菜单。
通过 ADO.NET Entity Framework (EF) 持久化 RBAC 数据
由于对装饰了我们的 `RBACAttribute` 授权属性的控制器操作的每个请求都需要验证请求用户在我们的 RBAC 数据库中存储的允许的应用程序权限,因此用户角色/权限数据应该只读取一次并持久化存储,以获得最大的性能。繁忙的网站每分钟可能面临数千个请求,为每个请求从 RBAC 数据库读取请求用户的角色/权限数据将严重影响网站的性能。
ADO.NET Entity Framework (EF) 是 .NET Framework 的一个组件,它使用一个称为实体数据模型 (EDM) 的概念模型提供数据持久化层,该模型位于数据库架构之上。EF 应用程序可以在安装了 .NET Framework(从 3.5 SP1 开始)的任何计算机上运行。EF 使用一个映射到底层数据库表的数据模型在内存中持久化数据;对底层表的更改会自动检测并自动刷新到持久化数据模型中。这使得 EF 成为存储我们的 RBAC 数据的理想选择(即使您现有的应用程序未使用 EF),因为性能将得到显著提高,因为 RBAC 数据不会在每次查询数据模型时都从底层数据库中读取。一旦我们的角色/权限配置正确,更新将很少。
创建实体框架 (EF) RBAC 模型
让我们创建一个新的 EF 模型来存储我们的 RBAC 数据,该数据将被我们的 RBACUser 对象使用。将一个新的“ADO.NET Entity Data Model”项添加到您的现有项目中,如下所示(除非您已经有一个模型,在这种情况下,您只需在模型的 `OnModelCreating` 方法中添加表关联)。
相应地命名您的数据库连接字符串。指定的名称将存储在应用程序的 Web.Config 文件中,作为位于
<connectionStrings>
<add name="RBAC_Model" connectionString="data source=localhost;initial..." providerName="System.Data.SqlClient" />
</connectionStrings>
生成的实体框架 (EF) RBAC 模型
以下数据库上下文模型和相应的数据库实体是从我们的 RBAC 数据库表中生成的;我们通过上下文模型访问应用程序的 RBAC 数据。
namespace RBAC.Models
{
public partial class RBAC_Model : DbContext
{
public RBAC_Model()
: base("name=RBAC_Model")
{
}
public virtual DbSet<Permission> Permissions { get; set; }
public virtual DbSet<Role> Roles { get; set; }
public virtual DbSet<User> Users { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Permission>()
.HasMany(e => e.Roles)
.WithMany(e => e.Permissions)
.Map(m => m.ToTable("LNK_ROLE_PERMISSION")
.MapLeftKey("Permission_Id").MapRightKey("Role_Id"));
modelBuilder.Entity<Role>()
.HasMany(e => e.Users)
.WithMany(e => e.Roles)
.Map(m => m.ToTable("LNK_USER_ROLE")
.MapLeftKey("Role_Id").MapRightKey("User_Id"));
}
}
}
[Table("USERS")]
public partial class User
{
public User()
{
Roles = new HashSet<Role>();
}
[Key]
public int User_Id { get; set; }
public string Username { get; set; }
public virtual ICollection<Role> Roles { get; set; }
}
[Table("ROLES")]
public partial class Role
{
public Role()
{
Permissions = new HashSet<Permission>();
Users = new HashSet<User>();
}
[Key]
public int Role_Id { get; set; }
public string RoleName { get; set; }
public string RoleDescription { get; set; }
public bool IsSysAdmin { get; set; }
public virtual ICollection<Permission> Permissions { get; set; }
public virtual ICollection<User> Users { get; set; }
}
[Table("PERMISSIONS")]
public partial class Permission
{
public Permission()
{
Roles = new HashSet<Role>();
}
[Key]
public int Permission_Id { get; set; }
public string PermissionDescription { get; set; }
public virtual ICollection<Role> Roles { get; set; }
}
生成的模型从我们的 RBAC 数据库中识别并定义了三个实体(User、Role 和 Permission),并定义了 `RBAC_Model` 上下文模型,该模型将实体关系链接在一起。在创建数据库上下文模型期间会调用 `RBAC_Model` 上下文模型类上的 `OnModelCreating` 方法,其中创建了模型实体关系,这些关系对应于底层数据库表,包括表索引键和引用完整性约束。
注意:如果您在现有应用程序中已经有一个实体数据模型 (EDM),只需将生成的实体类(Users、Roles 和 Permissions)添加到您的项目中,并在您的模型中定义这些实体的相应 `DbSet`,包括 `OnModelCreating` 方法中定义的实体关系。只要 RBAC 表已添加到您的现有数据库中,就不需要将数据库连接字符串添加到应用程序的 Web.Config 中,因为您的应用程序将已经为现有的 EDM 定义了连接字符串。
从我们的实体框架 (EF) RBAC 模型中提取数据
要创建我们的 RBAC EF 上下文模型的实例,我们只需创建一个 `RBAC_Model` 类的实例,然后从中搜索数据库实体(即 Users、Roles 和 Permissions)。以下列表演示了我们的上下文模型的创建以及对 Users 表的“swloch”的搜索。
//Create an instance of the RBAC Entity Data Model (EDM)...
using (RBAC_Model _data = new RBAC_Model())
{
User _user = _data.Users.Where(u => u.Username == 'swloch').FirstOrDefault();
if (_user != null)
{
//User found, access roles/permissions via exposed properties...
foreach (Role _role in _user.Roles)
{
foreach (Permission _permission in _role.Permissions)
{
if (_permission.PermissionDescription == 'admin-create')
{
}
}
}
}
}
第一次创建 `DbContext` 非常昂贵,但一旦对象创建,大部分信息都会被缓存,因此后续的实例化会快得多。您更有可能遇到性能问题是由于保留了一个上下文对象,而不是每次需要访问数据库时都实例化一个。如果您保留了一个上下文对象,它将跟踪所有更新、添加、删除等,这将减慢您的应用程序速度,甚至可能导致应用程序中出现细微的错误。
上下文对象应该为每个请求创建。创建上下文对象,做您需要做的事情,然后丢弃它。不要试图拥有一个全局上下文(Web 应用程序不是这样工作的)。
从数据库读取用户角色/权限
现在我们将更详细地看看我们 `RBACUser` 类中使用的 `GetDatabaseUserRolesPermissions()` 方法。
实体框架 (EF) RBAC 数据检索的代码列表
以下列表详细介绍了 `GetDatabaseUserRolesPermissions()` 方法,该方法使用 ADO.NET Entity Framework 从底层数据库中提取请求用户的 RBAC 数据。当第一次为 RBAC 上下文模型创建新实例时,数据将从数据库中检索并存储在内存中。因此,后续 RBAC 上下文模型的新实例不需要直接从底层数据库加载数据,因为 EF 将使用已存储在内存中的数据。EF 会检测数据库中数据的任何底层更改,并在我们下次访问数据时重新加载,从而确保从我们的“持久化数据层”检索的数据始终是最新的。
private void GetDatabaseUserRolesPermissions()
{
using (RBAC_Model _data = new RBAC_Model())
{
User _user = _data.Users.Where(u => u.UserName == this.Username).FirstOrDefault();
if (_user != null)
{
this.User_Id = _user.Id;
foreach (Role _role in _user.Roles)
{
UserRole _userRole = new UserRole {
Role_Id = _role.Id,
RoleName = _role.RoleName };
foreach (Permission _permission in _role.Permissions)
{
_userRole.Permissions.Add(new RolePermission {
Permission_Id = _permission.Id,
PermissionDescription = _permission.PermissionDescription });
}
this.Roles.Add(_userRole);
if (!this.IsSystemAdmin)
this.IsSystemAdmin = _role.IsSysAdmin;
}
}
}
}
扩展 RBAC 框架
如果我们想用自定义方法扩展我们的 RBAC 框架,我们只需在 `RBACUser` 类中添加新方法;这些方法可以与 USERS 数据库表中添加的新字段或关联表相关联。例如,我们可以向 USERS 数据库表添加一个名为“**Title**”的新列,该列代表用户的姓名头衔(例如,“Mr”、“Prof”、“Dr”等)。通过将新属性“**Title**”添加到 `RBACUser` 类中,如下所示,该属性将由 Entity Framework 自动从数据库加载到我们的 `RBACUser` 对象中。
然后,我们可以在 `RBACUser` 类中公开一个名为 `IsDoctor()` 的新方法,该方法检查值“**Dr**”并返回一个布尔值(如果值等于“**Dr**”则为 **true**,否则为 **false**)。虽然这是一个不太可能的实际示例,但它演示了概念。
public class RBACUser
{
public int User_Id { get; set; }
public bool IsSysAdmin { get; set; }
public string Username { get; set; }
private List<UserRole> Roles = new List<UserRole>();
public string Title { get; set; }
public bool IsDoctor()
{
return (this.Title == "Dr");
}
...
...
}
为了将我们的新函数暴露给应用程序的控制器操作和控制器视图,我们必须在我们 `RBAC_ExtendedMethods` 类(即我们的扩展方法)中包装新函数。
public static class RBAC_ExtendedMethods
{
public static bool IsDoctor(this ControllerBase controller)
{
bool IsDoctor = false;
try
{
//Check if the requesting user has the specified role...
IsDoctor = new RBACUser(controller.ControllerContext
.HttpContext.User.Identity.Name).IsDoctor();
}
catch { }
return IsDoctor;
}
...
...
}
我们现在已经通过自定义方法 `IsDoctor()` 扩展了我们的 RBAC 框架。我们现在可以通过 `this.IsDoctor()` 语法在控制器操作中使用新方法,或通过 `ViewContext.Controller.IsDoctor()` 语法在控制器视图中使用新方法。
注意:上述示例中使用的代码片段非常简短,仅突出显示其他代码,以专注于当前主题。
示例项目
可下载的示例项目实现了 `AdminController`,它提供了必要的 RBAC 管理。只需将此控制器、Admin 文件夹中的配套视图以及 RBAC 模型添加到任何现有的 MVC 项目中,即可提供必要的 RBAC 管理功能。下一节将讨论将 RBAC 管理功能集成到现有的 ASP.NET MVC 项目中。
RBAC 管理通过“System Administration”菜单暴露,如下所示,并且仅对拥有 `IsSysAdmin` 选项启用的角色的用户可见;菜单项定义必须包含在 `IsSysAdmin` 函数中以动态显示,如“使用 RBAC 动态控制菜单”部分所述。菜单样式由 CSS 驱动,可以轻松修改以遵循任何应用程序主题。
在创建任何应用程序角色之前,我们需要创建与我们的应用程序关联的权限。根据您需要使用基于角色的访问限制的应用程序区域,可能有大量的控制器操作方法,这些方法会转换为应用程序权限。将每个控制器操作作为权限输入到您的应用程序中可能是一项枯燥且耗时的任务。为了帮助创建您的应用程序权限,“Permissions”屏幕包含一个标有“Import Permissions”的按钮。
应用程序权限
导入权限功能使用 .NET Framework 的反射 API,该 API 允许在运行时获取程序集类型信息。该函数会遍历程序集的 MVC 控制器方法,并将每个控制器-操作保存到权限数据库表中。
应用程序角色
一旦定义了应用程序的权限,我们就可以创建用户角色。用户角色通常与一个或多个应用程序权限相关联。您的应用程序业务规则应定义您应用程序中的哪些角色应访问哪些区域。单击“Roles”菜单将显示您应用程序的角色(已定义),并允许对角色执行 CRUD 操作。
一旦创建了应用程序角色,就可以将应用程序权限分配给该角色。权限可以随时与角色关联和解除关联。将权限与角色关联可能是一项耗时的工作。为了帮助进行角色-权限关联过程,“Add All Permissions”按钮将所有权限一次性与角色关联;然后可以使用垃圾桶图标解除不需要的权限。或者,可以从下拉列表中选择单个权限并通过“Add Permission”按钮添加。
应用程序用户
一旦定义了应用程序的角色,就可以创建用户并为他们分配角色。
角色可以随时与用户关联和解除关联。通过从下拉列表中选择角色并按“Add Role”按钮将单个角色分配给用户;可以使用垃圾桶图标取消分配不需要的角色。
注意:通过“Roles”屏幕永久删除应用程序角色将自动从关联用户中删除该角色。
将 RBAC 添加到现有 MVC 应用程序
将 RBAC 功能添加到现有应用程序需要执行以下步骤:
|
![]() |
RBAC 身份验证/授权概述
IIS 用户身份验证过程由 IIS 执行,以提供额外的安全层。由于 Web 应用程序是内网应用程序,并且将在 Windows 域内运行,因此 IIS 将检查向 Web 服务器发出入站请求的用户是否已通过身份验证。如果用户未通过身份验证,IIS 会将入站请求重定向到身份验证登录对话框,要求提供替代用户凭据。如果用户已通过身份验证,则入站请求将转发到 MVC Web 应用程序,该应用程序将执行前面各节中所述的授权检查。
下图说明了 RBAC 身份验证/授权过程。
对我们 Web 应用程序的入站请求最初由 IIS 处理,如果用户未通过身份验证,IIS 会通过身份验证登录对话框将用户与 Active Directory 组进行身份验证。如果用户已通过身份验证,则请求将转发到 MVC Web 应用程序,该应用程序会检查用户的权限和角色。用户的权限将决定是否可以处理所请求的控制器/操作。用户作为对象存储在实体框架层中,该层从数据库表中填充用户的数据。
结论
此解决方案为任何需要动态的、自包含的、特定于应用程序且独立于 ASP.NET 角色和成员资格以及身份管理提供程序 (IdM)(如 Microsoft Identity Integration Server (MIIS))的基于角色的访问控制 (RBAC) 的内网应用程序提供了一个理想的框架。该框架可以添加到现有项目和新开发项目中,一旦部署,将由应用程序的系统管理员进行自我维护和管理,几乎不需要依赖应用程序开发人员。
此解决方案特别适用于公司内网应用程序,在这些应用程序中,对已部署 Web 服务器的访问受到限制,并且/或者用户角色的管理(包括角色分配)被委托给应用程序系统管理员和/或所有者。
在 下一篇文章 中,文章将扩展该框架以包含基于角色的报告,并重构 RBAC 以使用通过 HTTPS 的用户名/密码身份验证的 ASP.NET MVC Web 应用程序。