基于MVC .NET Core动态角色的授权
目录 (Table of Contents)
目标 (Goal)
Usually for small organisations, there are no predefined fixed roles/users. They learn processes while they grow and prosper over time. In such situations, we usually get requirements to create role and assign permissions dynamically without compromising security because mostly, those people giving requirements also not sure about the roles or policies. So here, we'll try to learn dynamic Role based Authorization.
通常对于小型组织,没有预定义的固定角色/用户。 他们在成长和繁荣的过程中学习过程。 在这种情况下,我们通常会获得创建角色和动态分配权限的要求,而又不会损害安全性,因为大多数提出要求的人还不确定角色或策略。 因此,在这里,我们将尝试学习基于角色的动态授权。
介绍 (Introduction)
In this article, we'll try to learn how to create dynamic roles and assign permissions dynamically to those roles. It's a continuation of a previous article, MVC 6 Dynamic Navigation Menu from Database.
在本文中,我们将尝试学习如何创建动态角色以及如何为这些角色动态分配权限。 这是上一篇文章“ 数据库的MVC 6动态导航菜单”的续篇。
Previously, we learnt how to generate the menu dynamically from database. Now according to that menu, we need to validate permissions for user roles. We'll learn to:
以前,我们学习了如何从数据库动态生成菜单。 现在,根据该菜单,我们需要验证用户角色的权限。 我们将学习:
- create a new role 创建一个新角色
- assign/remove role permissions dynamically 动态分配/删除角色权限
- assign/remove new role to User 向用户分配/删除新角色
使用的组件 (Components Used)
Here are the components that you'll need to build and test the demo code provided.
这是构建和测试所提供的演示代码所需的组件。
We'll be using .NET Core Framework version 3.1 with C# & MVC project template, so let's start.
我们将使用带有C#和MVC项目模板的.NET Core Framework 3.1版,因此开始吧。
From the previous article, I've added some extra fields like ExternalUrl
& DisplayOrder
to give an option to add external links in the menu and set the order of menu items as per user's choice.
在上一篇文章中,我添加了一些额外的字段,例如ExternalUrl
& DisplayOrder
提供在菜单中添加外部链接的选项,并根据用户的选择设置菜单项的顺序。
建立新专案 (Create New Project)
Open Visual Studio 2019 and click on Create a new project to start with a new project.
打开Visual Studio 2019,然后单击创建新项目以开始新项目。
It'll show you the below screen for more selections, so select C#, All platforms, Web and then ASP.NET Core Web Application and click Next.
它将在下面的屏幕中显示更多选择,因此选择C# , 所有平台 , Web ,然后选择ASP.NET Core Web Application并单击Next 。
Here, we need to provide the project name and click on Create.
在这里,我们需要提供项目名称,然后单击Create 。
Select .NET Core, ASP.NET Core 3.1, Model-View-Controller as the template and Individual User Accounts as Authentication, then click on Create, Visual Studio will create a new project with all these settings for you.
选择.NET Core , ASP.NET Core 3.1 , Model-View-Controller作为模板,选择个人用户帐户作为身份验证,然后单击Create ,Visual Studio将为您创建一个具有所有这些设置的新项目。
After setting up the project, let's create database based on our model, make sure to setup the connection string in appsettings.json file. I'll be using the localhost as my server with Windows authentication, following is my connection string.
设置项目后,让我们基于模型创建数据库,确保在appsettings.json文件中设置连接字符串。 我将使用本地主机作为Windows身份验证的服务器,以下是我的连接字符串。
"DefaultConnection": "Server=localhost;Database=DynamicPermissions;
Trusted_Connection=True;MultipleActiveResultSets=true"
I've created NavigationMenu
to store menu names & RoleMenuPermission
entity to store role permissions.
我创建了NavigationMenu
来存储菜单名称,并创建了RoleMenuPermission
实体来存储角色权限。
[Table(name: "AspNetNavigationMenu")]
public class NavigationMenu
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public string Name { get; set; }
[ForeignKey("ParentNavigationMenu")]
public Guid? ParentMenuId { get; set; }
public virtual NavigationMenu ParentNavigationMenu { get; set; }
public string Area { get; set; }
public string ControllerName { get; set; }
public string ActionName { get; set; }
public bool IsExternal { get; set; }
public string ExternalUrl { get; set; }
public int DisplayOrder { get; set; }
[NotMapped]
public bool Permitted { get; set; }
public bool Visible { get; set; }
}
[Table(name: "AspNetRoleMenuPermission")]
public class RoleMenuPermission
{
public string RoleId { get; set; }
public Guid NavigationMenuId { get; set; }
public NavigationMenu NavigationMenu { get; set; }
}
Here is my Db Context, we're overriding OnModelCreating
to define RoleId
& NavigationMenuId
as keys because we don't need an identity key for this table.
这是我的Db上下文,我们重写OnModelCreating
来将RoleId
和NavigationMenuId
定义为键,因为我们不需要此表的身份键。
public class ApplicationDbContext : IdentityDbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<RoleMenuPermission> RoleMenuPermission { get; set; }
public DbSet<NavigationMenu> NavigationMenu { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<RoleMenuPermission>()
.HasKey(c => new { c.RoleId, c.NavigationMenuId});
base.OnModelCreating(builder);
}
}
移居 (Migrations)
Now we need to run the migrations and then update the database, Enable-Migrations command has been obsolete, so we need to delete everything from Migrations folder and then run add migration command.
现在我们需要运行迁移,然后更新数据库, Enable-Migrations命令已过时 ,因此我们需要从Migrations文件夹中删除所有内容,然后运行add migration命令。
add-migration InitialVersion
Here are my database tables like shown below:
这是我的数据库表,如下所示:
For more details on Seeding data, you can check out the following article:
有关播种数据的更多详细信息,可以查看以下文章:
New version of EF .NET Core has HasData
on ModelBuilder
object in OnModelCreating
function but for now, we'll stick to the above approach for this demonstration.
EF .NET Core的新版本在ModelBuilder
上具有HasData
OnModelCreating
对象 功能,但现在,在本演示中,我们将继续使用上述方法。
Modification in DbInitializer
, added new Permissions and allocated to Admin Role, we need these to be available in the database so we can allocate and validate for User roles later.
在DbInitializer
修改,添加了新的权限并分配给管理员角色,我们需要这些权限在数据库中可用,以便稍后可以分配和验证用户角色。
new NavigationMenu()
{
Id = new Guid("F704BDFD-D3EA-4A6F-9463-DA47ED3657AB"),
Name = "External Google Link",
ControllerName = "",
ActionName = "",
IsExternal = true,
ExternalUrl = "https://www.google.com/",
ParentMenuId = new Guid("13e2f21a-4283-4ff8-bb7a-096e7b89e0f0"),
DisplayOrder=2,
Visible = true,
},
new NavigationMenu()
{
Id = new Guid("913BF559-DB46-4072-BD01-F73F3C92E5D5"),
Name = "Create Role",
ControllerName = "Admin",
ActionName = "CreateRole",
ParentMenuId = new Guid("13e2f21a-4283-4ff8-bb7a-096e7b89e0f0"),
DisplayOrder=3,
Visible = true,
},
new NavigationMenu()
{
Id = new Guid("3C1702C5-C34F-4468-B807-3A1D5545F734"),
Name = "Edit User",
ControllerName = "Admin",
ActionName = "EditUser",
ParentMenuId = new Guid("13e2f21a-4283-4ff8-bb7a-096e7b89e0f0"),
DisplayOrder=3,
Visible = false,
},
new NavigationMenu()
{
Id = new Guid("94C22F11-6DD2-4B9C-95F7-9DD4EA1002E6"),
Name = "Edit Role Permission",
ControllerName = "Admin",
ActionName = "EditRolePermission",
ParentMenuId = new Guid("13e2f21a-4283-4ff8-bb7a-096e7b89e0f0"),
DisplayOrder=3,
Visible = false,
},
I have added two new functions in the data service from our previous implementation.
在先前的实现中,我在数据服务中添加了两个新功能。
We'll get all defined permissions from NavigationMenu
joining with allocated to role having Permitted = true
so based on that, we can render the checkboxes Checked/Unchecked.
我们将从NavigationMenu
获得所有已定义的权限,并分配给具有Permitted = true
角色,因此可以基于此呈现复选框Checked / Unchecked。
public async Task<List<NavigationMenuViewModel>> GetPermissionsByRoleIdAsync(string id)
{
var items = await (from m in _context.NavigationMenu
join rm in _context.RoleMenuPermission
on new { X1 = m.Id, X2 = id } equals
new { X1 = rm.NavigationMenuId, X2 = rm.RoleId }
into rmp
from rm in rmp.DefaultIfEmpty()
select new NavigationMenuViewModel()
{
Id = m.Id,
Name = m.Name,
Area = m.Area,
ActionName = m.ActionName,
ControllerName = m.ControllerName,
IsExternal = m.IsExternal,
ExternalUrl = m.ExternalUrl,
DisplayOrder = m.DisplayOrder,
ParentMenuId = m.ParentMenuId,
Visible = m.Visible,
Permitted = rm.RoleId == id
})
.AsNoTracking()
.ToListAsync();
return items;
}
//Remove old permissions for that role id and assign changed permissions
public async Task<bool> SetPermissionsByRoleIdAsync(string id, IEnumerable<Guid> permissionIds)
{
var existing = await _context.RoleMenuPermission.Where(x => x.RoleId == id).ToListAsync();
_context.RemoveRange(existing);
foreach (var item in permissionIds)
{
await _context.RoleMenuPermission.AddAsync(new RoleMenuPermission()
{
RoleId = id,
NavigationMenuId = item,
});
}
var result = await _context.SaveChangesAsync();
return result > 0;
}
Here is my Admin Controller, for detailed implementation for actions, we can see code in the zip. Simple implementation, no magic code :). We just need to put [Authorize("Authorization")]
on any Action we want to tell the application to validate authorization or it can be used on Controller level if all the actions are protected.
这是我的管理控制器,有关操作的详细实现,我们可以在zip中查看代码。 简单的实现,没有魔术代码:)。 我们只需要将[Authorize("Authorization")]
放在我们要告诉应用程序验证授权的任何操作上,或者如果所有操作都受到保护,则可以在控制器级别上使用它。
[Authorize("Authorization")]
public class AdminController : Controller
{
private readonly UserManager<IdentityUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
private readonly IDataAccessService _dataAccessService;
private readonly ILogger<AdminController> _logger;
public AdminController(
UserManager<IdentityUser> userManager,
RoleManager<IdentityRole> roleManager,
IDataAccessService dataAccessService,
ILogger<AdminController> logger)
{
_userManager = userManager;
_roleManager = roleManager;
_dataAccessService = dataAccessService;
_logger = logger;
}
public async Task<IActionResult> Roles() {}
[HttpPost]
public async Task<IActionResult> CreateRole(RoleViewModel viewModel) {}
public async Task<IActionResult> Users() {}
public async Task<IActionResult> EditUser(string id){}
[HttpPost]
public async Task<IActionResult> EditUser(UserViewModel viewModel){}
public async Task<IActionResult> EditRolePermission(string id){}
[HttpPost]
public async Task<IActionResult> EditRolePermission
(string id, List<NavigationMenuViewModel> viewModel){}
}
Here is how we render the checkboxes list.
这是我们呈现复选框列表的方式。
<form asp-action="EditRolePermission">
<div class="form-group">
<ul style="list-style-type: none;">
@for (var i = 0; i < Model.Count; i++)
{
<li>
<input type="checkbox" asp-for="@Model[i].Permitted" />
<label style="margin-left:10px;"
asp-for="@Model[i].Permitted">@Model[i].Name</label>
<input type="hidden" asp-for="@Model[i].Id" />
<input type="hidden" asp-for="@Model[i].Name" />
</li>
}
</ul>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
<a asp-action="Roles">Back to List</a>
</div>
</form>
So now we can run & test the system with Admin User by logging in with:
现在,我们可以通过以下方式使用“管理员用户”来运行和测试系统:
- Username: aaa@qq.com 用户名:aaa@qq.com
- Password: aaa@qq.com 密码:P @ ssw0rd
角色,创建角色 (Roles, Create Role)
Here is the list of roles which were created as part of migrations:
以下是在迁移过程中创建的角色列表:
From Create Role screen, a new role can be added in the system.
在“ 创建角色”屏幕中,可以在系统中添加新角色。
分配角色权限 (Assign Role Permissions)
In Roles Listing, if we click on Edit Permissions button, it'll take us to Permissions screen listing all permissions with allocated permissions checked.
在“角色列表”中,如果单击“ 编辑权限”按钮,它将带我们进入“权限”屏幕,其中列出了已选中分配的权限的所有权限。
Now we can change these permissions and save to make it effective for users under that role. So let's try to change it.
现在,我们可以更改这些权限并进行保存,以使其对该角色下的用户有效。 因此,让我们尝试更改它。
We'll uncheck External Google Link & Create Role.
我们将取消选中“外部Google链接和创建角色”。
Now when I'll save these changes and after that again Edit permissions for that same role.
现在,当我保存这些更改,然后再次保存该角色的“编辑”权限。
As you can see, now those two permissions are unchecked and not present in the menu as well.
如您所见,现在这两个权限都未选中,并且菜单中也不存在。
Now I can try to access Create Role page by pasting the URL, so it should validate me according to my updated permissions and throw me an Access Denied.
现在,我可以尝试通过粘贴URL来访问“ 创建角色”页面,因此它应该根据我的更新权限对我进行验证,并向我抛出“访问被拒绝”。
Same can be verified if we copy URL for some page with user having access, then login with some other user without access to that page and paste the copied URL, it should give the same error.
如果我们复制具有访问权限的某个页面的URL的URL,然后与其他无法访问该页面的用户登录并粘贴复制的URL,则可以验证相同的错误。
分配角色给用户 (Assign Role to User)
We can see the user listing with Edit button.
我们可以使用“ 编辑”按钮查看用户列表。
By Editing, we'll be able to assign/remove roles to User, after we click on Edit button, we can see the below screen with list of all roles as check box list available in the system.
通过编辑,我们将能够为用户分配/删除角色,单击“ 编辑”按钮后,我们可以看到以下屏幕,其中包含所有角色的列表作为系统中可用的复选框列表。
So now we have screens to Create new role, Roles listing, Edit User, Edit permissions for role, based on these interfaces, we need to validate the authorization.
因此,现在我们有了基于这些界面的用于创建新角色,角色列表,编辑用户,角色的编辑权限的屏幕,我们需要验证授权。
存取限制 (Access Restriction)
We'll use Authorization handler for that purpose but instead of many policies or roles already defined at the time of development, in real world systems, roles can be changed and reassigned to different users or one user can have multiple roles for some specific period of time, etc. Keeping that in mind, we'll give the liberty to the end user to give permissions to their defined roles so their customer/employees with those roles could perform their duties according to their roles and permissions.
我们将为此目的使用“授权处理程序”,但在现实世界的系统中,可以更改角色并将其重新分配给不同的用户,或者在一个特定的时间段内,一个用户可以具有多个角色,而不是在开发时已经定义许多策略或角色。请牢记这一点,我们将给予最终用户*以授予对其定义的角色的权限,以便具有这些角色的客户/员工可以根据其角色和权限来执行其职责。
We'll generalize AuthorizationHandler
to make it work dynamically with permissions from the database. We need to Create an Authorization requirement and inherit from IAuthorizationRequirement
interface. Now we can create an AuthorizationHandler
and pass our requirement using generics, then we can override the HandleRequirementAsync
function. To get Controller and Action from end point and check for permission from database. With this approach, Authorization will be coupled to MVC but that's ok because that handler has been written for this particular purpose & use.
我们将泛化AuthorizationHandler
使其在数据库的权限下动态地工作。 我们需要创建一个授权需求,并从IAuthorizationRequirement
接口继承。 现在,我们可以创建一个AuthorizationHandler
并使用泛型传递我们的要求,然后可以覆盖HandleRequirementAsync
函数。 从端点获取Controller和Action并从数据库中检查权限。 通过这种方法,授权将与MVC耦合,但这没关系,因为已针对该特定目的和用途编写了该处理程序。
public class AuthorizationRequirement : IAuthorizationRequirement { }
public class PermissionHandler : AuthorizationHandler<AuthorizationRequirement>
{
private readonly IDataAccessService _dataAccessService;
public PermissionHandler(IDataAccessService dataAccessService)
{
_dataAccessService = dataAccessService;
}
protected async override Task HandleRequirementAsync(AuthorizationHandlerContext context,
AuthorizationRequirement requirement)
{
if (context.Resource is RouteEndpoint endpoint)
{
endpoint.RoutePattern.RequiredValues
.TryGetValue("controller", out var _controller);
endpoint.RoutePattern.RequiredValues
.TryGetValue("action", out var _action);
endpoint.RoutePattern.RequiredValues.TryGetValue("page", out var _page);
endpoint.RoutePattern.RequiredValues.TryGetValue("area", out var _area);
var isAuthenticated = context.User.Identity.IsAuthenticated;
if (isAuthenticated && _controller != null && _action != null &&
await _dataAccessService.GetMenuItemsAsync(context.User,
_controller.ToString(), _action.ToString()))
{
context.Succeed(requirement);
}
}
}
}
For demonstration and limited scope of our discussion, we'll be checking/validating it on the fly from database, to improve the performance, we can use Cache to hold permissions to reduce database calls for authorization checks on each resource access. Role Permissions can be added to user claims and permissions in Cache to give it a performance boost.
为了演示和限制讨论范围,我们将从数据库中即时检查/验证它,以提高性能,我们可以使用Cache持有权限来减少对每个资源访问进行授权检查的数据库调用。 可以将角色权限添加到缓存中的用户声明和权限,以提高性能。
结论 (Conclusion)
We have created our database through migrations and started our project under Development environment. Logged in User can see Menu Items and pages according to dynamically defined role permissions. The source code is attached. I encourage you to download the sample code, run and see. All of you are most welcome to post comments if you have any questions/suggestions.
我们已经通过迁移创建了数据库,并在开发环境下启动了我们的项目。 登录的用户可以根据动态定义的角色权限查看菜单项和页面。 源代码已随附。 我鼓励您下载示例代码,运行并查看。 如果您有任何疑问/建议,我们非常欢迎大家发表评论。
Thanks for reading...
谢谢阅读...
有趣的读物 (Interesting Reads)
Between developer community and .NET Core security team, some discussions are still going on.
在开发人员社区和.NET Core安全团队之间 ,一些讨论仍在进行中。
-
Revisit PolicyProvider (large # permissions) scenarios/support #917
历史 (History)
-
5th March, 2020: Initial version
2020年3月5 日 :初始版本
翻译自: https://www.codeproject.com/Articles/5165567/MVC-NET-Core-Dynamic-Role-Based-Authorization
推荐阅读
-
基于MVC .NET Core动态角色的授权
-
构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(24)-权限管理系统-将权限授权给角色
-
.Net Core权限认证基于Cookie的认证&授权.Scheme、Policy扩展
-
构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(24)-权限管理系统-将权限授权给角色
-
基于Asp.Net Core MVC和AdminLTE的响应式管理后台之侧边栏处理
-
ASP.NET Core 3.0 一个 jwt 的轻量角色/用户、单个API控制的授权认证库
-
.NET Core的响应式框架,基于Ace Admin框架菜单导航,Bootstrap布局,fontAwesome图标,内嵌Iframe用EasyUI做数据绑定,动态配置列表,动态配置表单
-
asp.net core MVC之实现基于token的认证
-
使用Asp.Net Core MVC 开发项目实践[第四篇:基于EF Core的扩展2]
-
ASP.net MVC 基于角色的权限控制系统的实现