ASP.NET Core 认证与授权[7]:动态授权
ASP.NET Core 中基于策略的授权旨在分离授权与应用程序逻辑,它提供了灵活的策略定义模型,在一些权限固定的系统中,使用起来非常方便。但是,当要授权的资源无法预先确定,或需要将权限控制到每一个具体的操作当中时,基于策略的授权便不再适用,本章就来介绍一下如何进行动态的授权。
目录
基于资源的授权
有些场景下,授权需要依赖于要访问的资源,例如:每个资源通常会有一个创建者属性,我们只允许该资源的创建者才可以对其进行编辑,删除等操作,这就无法通过[Authorize]
特性来指定授权了。因为授权过滤器会在我们的应用代码,以及MVC的模型绑定之前执行,无法确定所访问的资源。此时,我们需要使用基于资源的授权,下面就来演示一下具体是如何操作的。
定义资源Requirement
在基于资源的授权中,我们要判断的是用户是否具有针对该资源的某项操作,因此,我们先定义一个代表操作的Requirement:
public class MyRequirement : IAuthorizationRequirement
{
public string Name { get; set; }
}
可以根据实际场景来定义需要的属性,在本示例中,只需要一个Name
属性,用来表示针对资源的操作名称(如:增查改删等)。
然后,我们预定义一些常用的操作,方便业务中的调用:
public static class Operations
{
public static MyRequirement Create = new MyRequirement { Name = "Create" };
public static MyRequirement Read = new MyRequirement { Name = "Read" };
public static MyRequirement Update = new MyRequirement { Name = "Update" };
public static MyRequirement Delete = new MyRequirement { Name = "Delete" };
}
上面定义的 MyRequirement 虽然很简单,但是非常通用,因此,在 ASP.NET Core 中也内置了一个OperationAuthorizationRequirement
:
public class OperationAuthorizationRequirement : IAuthorizationRequirement
{
public string Name { get; set; }
}
在实际应用中,我们可以直接使用OperationAuthorizationRequirement
,而不需要再自定义 Requirement,而在这里只是为了方便理解,后续也继续使用 MyRequirement 来演示。
实现资源授权Handler
每一个 Requirement 都需要有一个对应的 Handler,来完成授权逻辑,可以直接让 Requirement 实现IAuthorizationHandler
接口,也可以单独定义授权Handler,在这里使用后者。
在本示例中,我们是根据资源的创建者来判断用户是否具有操作权限,因此,我们定义一个资源创建者的接口,而不是直接依赖于具体的资源:
public interface IDocument
{
string Creator { get; set; }
}
然后实现我们的授权Handler:
public class DocumentAuthorizationHandler : AuthorizationHandler<OperationAuthorizationRequirement, IDocument>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, IDocument resource)
{
// 如果是Admin角色就直接授权成功
if (context.User.IsInRole("admin"))
{
context.Succeed(requirement);
}
else
{
// 允许任何人创建或读取资源
if (requirement == Operations.Create || requirement == Operations.Read)
{
context.Succeed(requirement);
}
else
{
// 只有资源的创建者才可以修改和删除
if (context.User.Identity.Name == resource.Creator)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
}
return Task.CompletedTask;
}
}
在前面章节的《自定义策略》示例中,我们继承的是AuthorizationHandler<NameAuthorizationRequirement>
,而这里继承了AuthorizationHandler<OperationAuthorizationRequirement, Document>
,很明显,比之前的多了resource
参数,以便用来实现基于资源的授权。
如上,我们并没有验证用户是否已登录,以及context.User
是否为空等。这是因为在 ASP.NET Core 的默认授权中,已经对这些进行了判断,我们只需要在要授权的控制器上添加[Authorize]
特性即可,无需重复性的工作。
最后,不要忘了,还需要将DocumentAuthorizationHandler
注册到DI系统中:
services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();
调用AuthorizationService
现在就可以在我们的应用代码中调用IAuthorizationService
来完成授权了,不过在此之前,我们再来回顾一下IAuthorizationService
接口:
public interface IAuthorizationService
{
Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements);
Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName);
}
在《上一章》中,我们提到,使用[Authorize]
设置授权时,其AuthorizationHandlerContext
中的resource
字段被设置为空,现在,我们将要授权的资源传进去即可:
[Authorize]
public class DocumentsController : Controller
{
public async Task<ActionResult> Details(int? id)
{
var document = _docStore.Find(id.Value);
if (document == null)
{
return NotFound();
}
if ((await _authorizationService.AuthorizeAsync(User, document, Operations.Read)).Succeeded)
{
return View(document);
}
else
{
return new ForbidResult();
}
}
public async Task<IActionResult> Edit(int? id)
{
var document = _docStore.Find(id.Value);
if (document == null)
{
return NotFound();
}
if ((await _authorizationService.AuthorizeAsync(User, document, Operations.Update)).Succeeded)
{
return View(document);
}
else
{
return new ForbidReuslt();
}
}
}
如上,在授权失败时,我们返回了ForbidResult
,建议不要返回ChallengeResult
,因为我们要明确的告诉用户是无权访问,而不是未登录。
基于资源的权限非常简单,但是每次都要在应用代码中显示调用
IAuthorizationService
,显然比较繁琐,我们也可以使用AOP模式,或者使用EF Core拦截器来实现,将授权验证与业务代码分离。
基于权限的授权
在一个通用的用户权限管理系统中,通常每一个Action都代表一种权限,用户拥有哪些权限也是可以动态分配的。本小节就来介绍一下在 ASP.NET Core 中,如何实现一个简单权限管理系统。
定义权限项
首先,我们要确定我们的系统分为哪些权限项,这通常是由业务所决定的,并且是预先确定的,我们可以硬编码在代码中,方便统一调用:
public static class Permissions
{
public const string User = "User";
public const string UserCreate = "User.Create";
public const string UserRead = "User.Read";
public const string UserUpdate = "User.Update";
public const string UserDelete = "User.Delete";
}
如上,我们简单定义了“创建用户”,“查询用户”,“更新用户”,“删除用户”四个权限。通常会对权限项进行分组,构成一个树形结构,这样在展示和配置权限时,都会方便很多。在这里,使用.
来表示层级进行分组,其中User
权限项包含所有以User.
开头的权限。
定义权限Requirement
与基于资源的授权类似,我们同样需要定义一个权限Requirement:
public class PermissionAuthorizationRequirement : IAuthorizationRequirement
{
public PermissionAuthorizationRequirement(string name)
{
Name = name;
}
public string Name { get; set; }
}
使用Name
属性来表示权限的名称,与上面Permissions
的常量对应。
实现权限授权Handler
然后实现与上面定义的 Requirement 对应的授权Handler:
public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionAuthorizationRequirement>
{
private readonly UserStore _userStore;
public PermissionAuthorizationHandler(UserStore userStore)
{
_userStore = userStore;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionAuthorizationRequirement requirement)
{
if (context.User != null)
{
if (context.User.IsInRole("admin"))
{
context.Succeed(requirement);
}
else
{
var userIdClaim = context.User.FindFirst(_ => _.Type == ClaimTypes.NameIdentifier);
if (userIdClaim != null)
{
if (_userStore.CheckPermission(int.Parse(userIdClaim.Value), requirement.Name))
{
context.Succeed(requirement);
}
}
}
}
return Task.CompletedTask;
}
}
如上,把admin
角色设置为内部固定角色,直接跳过授权检查。其他角色则从Claims中取出用户Id,然后调用CheckPermission
完成授权。
权限检查的具体逻辑就属于业务层面的了,通常会从数据库中查找用的的权限列表进行验证,这里就不在多说,简单模拟了一下:
public class UserStore
{
private static List<User> _users = new List<User>() {
new User { Id=1, Name="admin", Password="111111", Role="admin", Email="admin@gmail.com", PhoneNumber="18800000000"},
new User { Id=2, Name="alice", Password="111111", Role="user", Email="alice@gmail.com", PhoneNumber="18800000001", Permissions = new List<UserPermission> {
new UserPermission { UserId = 1, PermissionName = Permissions.User },
new UserPermission { UserId = 1, PermissionName = Permissions.Role }
}
},
new User { Id=3, Name="bob", Password="111111", Role = "user", Email="bob@gmail.com", PhoneNumber="18800000002", Permissions = new List<UserPermission> {
new UserPermission { UserId = 2, PermissionName = Permissions.UserRead },
new UserPermission { UserId = 2, PermissionName = Permissions.RoleRead }
}
},
};
public bool CheckPermission(int userId, string permissionName)
{
var user = Find(userId);
if (user == null) return false;
return user.Permissions.Any(p => permissionName.StartsWith(p.PermissionName));
}
}
最后,与上面示例一样,将Handler注册到DI系统中:
services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
使用策略授权
那么,怎么在应用代码中使用基于权限的授权呢?
最为简单的,我们可以直接借助于 ASP.NET Core 的授权策略来实现基于权限的授权,因为此时并不需要资源。
services.AddAuthorization(options =>
{
options.AddPolicy(Permissions.UserCreate, policy => policy.AddRequirements(new PermissionAuthorizationRequirement(Permissions.UserCreate)));
options.AddPolicy(Permissions.UserRead, policy => policy.AddRequirements(new PermissionAuthorizationRequirement(Permissions.UserRead)));
options.AddPolicy(Permissions.UserUpdate, policy => policy.AddRequirements(new PermissionAuthorizationRequirement(Permissions.UserUpdate)));
options.AddPolicy(Permissions.UserDelete, policy => policy.AddRequirements(new PermissionAuthorizationRequirement(Permissions.UserDelete)));
});
如上,针对每一个权限项都定义一个对应的授权策略,然后,就可以在控制器中直接使用[Authorize]
来完成授权:
[Authorize]
public class UserController : Controller
{
[Authorize(Policy = Permissions.UserRead)]
public ActionResult Index()
{
}
[Authorize(Policy = Permissions.UserRead)]
public ActionResult Details(int? id)
{
}
[Authorize(Policy = Permissions.UserCreate)]
public ActionResult Create()
{
return View();
}
[Authorize(Policy = Permissions.UserCreate)]
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create([Bind("Title")] User user)
{
}
}
当然,我们也可以像基于资源的授权那样,在应用代码中调用IAuthorizationService
完成授权,这样做的好处是无需定义策略,但是,显然一个一个来定义策略太过于繁琐。
还有一种更好方式,就是使用MVC过滤器来完成对IAuthorizationService
的调用,下面就来演示一下。
自定义授权过滤器
我们可以参考上一章中介绍的《AuthorizeFilter》来自定义一个权限过滤器:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class PermissionFilter : Attribute, IAsyncAuthorizationFilter
{
public PermissionFilter(string name)
{
Name = name;
}
public string Name { get; set; }
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var authorizationService = context.HttpContext.RequestServices.GetRequiredService<IAuthorizationService>();
var authorizationResult = await authorizationService.AuthorizeAsync(context.HttpContext.User, null, new PermissionAuthorizationRequirement(Name));
if (!authorizationResult.Succeeded)
{
context.Result = new ForbidResult();
}
}
}
上面的实现非常简单,我们接受一个name
参数,代表权限的名称,然后将权限名称转化为PermissionAuthorizationRequirement
,最后直接调用 authorizationService 来完成授权。
接下来,我们就可以直接在控制器中使用PermissionFilter
过滤器来完成基于权限的授权了:
[Authorize]
public class UserController : Controller
{
[PermissionFilter(Permissions.UserRead)]
public ActionResult Index()
{
return View(_userStore.GetAll());
}
[PermissionFilter(Permissions.UserCreate)]
public ActionResult Create()
{
}
[PermissionFilter(Permissions.UserCreate)]
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create([Bind("Title")] User user)
{
}
[PermissionFilter(Permissions.UserUpdate)]
public IActionResult Edit(int? id)
{
}
[PermissionFilter(Permissions.UserUpdate)]
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(int id, [Bind("Id,Title")] User user)
{
}
}
在视图中使用授权
通常,在前端页面当中,我们也需要根据用户的权限来判断是否显示“添加”,“删除”等按钮,而不是让用户点击“添加”,再提示用户没有权限,这在 ASP.NET Core 中实现起来也非常简单。
我们可以直接在Razor视图中注入IAuthorizationService
来检查用户权限:
@inject IAuthorizationService AuthorizationService
@if ((await AuthorizationService.AuthorizeAsync(User, AuthorizationSample.Authorization.Permissions.UserCreate)).Succeeded)
{
<p>
<a asp-action="Create">创建</a>
</p>
}
不过,上面的代码是通过策略名称来授权的,如果我们使用了上面创建的授权过滤器,而没有定义授权策略的话,需要使用如下方式来实现:
@inject IAuthorizationService AuthorizationService
@if ((await AuthorizationService.AuthorizeAsync(User, new PermissionAuthorizationRequirement(AuthorizationSample.Authorization.Permissions.UserCreate))).Succeeded)
{
<p>
<a asp-action="Create">创建</a>
</p>
}
我们也可以定义一个AuthorizationService
的扩展方法,实现通过权限名称进行授权,这里就不再多说。
我们不能因为隐藏了操作按钮,就不在后端进行授权验证了,就像JS的验证一样,前端的验证就为了提升用户的体验,后端的验证在任何时候都是必不可少的。
总结
在大多数场景下,我们只需要使用授权策略就可以应对,而在授权策略不能满足我们的需求时,由于 ASP.NET Core 提供了一个统一的 IAuthorizationService
授权接口,这就使我们扩展起来也非常方便。ASP.NET Core 的授权部分到这来也就介绍完了,总的来说,要比ASP.NET 4.x的时候,简单,灵活很多,可见 ASP.NET Core 不仅仅是为了跨平台,而是为了适应现代应用程序的开发方式而做出的全新的设计,我们也应该用全新的思维去学习.NET Core,踏上时代的浪潮。
ASP.NET Core 认证与授权[7]:动态授权的更多相关文章
- ASP.NET Core 认证与授权[2]:Cookie认证
由于HTTP协议是无状态的,但对于认证来说,必然要通过一种机制来保存用户状态,而最常用,也最简单的就是Cookie了,它由浏览器自动保存并在发送请求时自动附加到请求头中.尽管在现代Web应用中,Coo ...
- ASP.NET Core WebApi基于JWT实现接口授权验证
一.ASP.Net Core WebApi JWT课程前言 我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再 ...
- ASP.NET Core 3.0中使用动态控制器路由
原文:Dynamic controller routing in ASP.NET Core 3.0 作者:Filip W 译文:https://www.cnblogs.com/lwqlun/p/114 ...
- 从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
OpenIDConnect是一个身份验证服务,而Oauth2.0是一个授权框架,在前面几篇文章里通过IdentityServer4实现了基于Oauth2.0的客户端证书(Client_Credenti ...
- ASP.NET Core 认证与授权[1]:初识认证
在ASP.NET 4.X 中,我们最常用的是Forms认证,它既可以用于局域网环境,也可用于互联网环境,有着非常广泛的使用.但是它很难进行扩展,更无法与第三方认证集成,因此,在 ASP.NET Cor ...
- ASP.NET Core 认证与授权[3]:OAuth & OpenID Connect认证
在上一章中,我们了解到,Cookie认证是一种本地认证方式,通常认证与授权都在同一个服务中,也可以使用Cookie共享的方式分开部署,但局限性较大,而如今随着微服务的流行,更加偏向于将以前的单体应用拆 ...
- ASP.NET Core 认证与授权[4]:JwtBearer认证
在现代Web应用程序中,通常会使用Web, WebApp, NativeApp等多种呈现方式,而后端也由以前的Razor渲染HTML,转变为Stateless的RESTFulAPI,因此,我们需要一种 ...
- ASP.NET Core 认证与授权[5]:初识授权
经过前面几章的姗姗学步,我们了解了在 ASP.NET Core 中是如何认证的,终于来到了授权阶段.在认证阶段我们通过用户令牌获取到用户的Claims,而授权便是对这些的Claims的验证,如:是否拥 ...
- ASP.NET Core 认证与授权[6]:授权策略是怎么执行的?
在上一章中,详细介绍了 ASP.NET Core 中的授权策略,在需要授权时,只需要在对应的Controler或者Action上面打上[Authorize]特性,并指定要执行的策略名称即可,但是,授权 ...
随机推荐
- Opencv怎么读入,显示,保存图像-OpenCV步步精深
怎么读入图像呢? 我们用 img = cv2.imread('图像路径\原图像名称',0) 原图像名称要有后缀 .png , .jpg等等原图像带有的后缀. 这里我们着重说明一下图像路径,这个路径一定 ...
- SpringBoot01 InteliJ IDEA安装、Maven配置、创建SpringBoot项目、属性配置、多环境配置
1 InteliJ IDEA 安装 下载地址:点击前往 注意:需要下载专业版本的,注册码在网上随便搜一个就行啦 2 MAVEN工具的安装 2.1 获取安装包 下载地址:点击前往 2.2 安装过程 到官 ...
- invalid types 'int[int]' for array subscript// EOF 输入多组数据//如何键盘输入EOF
数组维度搞错了 一次运行,要输入多组数据,直到读至输入文件末尾(EOF)为止 while(scanf("%d %d",&a, &b) != EOF) // 输入结束 ...
- win10 uwp 装机必备应用 含源代码
zhxilin大神说http://www.cnblogs.com/zhxilin/p/4819372.html这文章说到了使用await Windows.System.Launcher.LaunchU ...
- Latex 论文elsevier,手把手如何用Latex写论文
这几天在开始写论文,准备发的是elsevier,这个网站的instruction有问题,下载的东西基本上好多的错误,所以我就写博客记录. 首先看下:https://www.elsevier.com/a ...
- JavaScript 开发人员需要知道的简写技巧
本文来源于多年的 JavaScript 编码技术经验,适合所有正在使用 JavaScript 编程的开发人员阅读. 本文的目的在于帮助大家更加熟练的运用 JavaScript 语言来进行开发工作. 文 ...
- python第五课——自定义线程池
内容概要: 1.low版线程池 2.绝版线程池 1.low版线程池 设计思路:运用队列queue 将线程类名放入队列中,执行一个就拿一个出来 import queue import threading ...
- 使用selenium webdriver+beautifulsoup+跳转frame,实现模拟点击网页下一页按钮,抓取网页数据
记录一次快速实现的python爬虫,想要抓取中财网数据引擎的新三板板块下面所有股票的公司档案,网址为http://data.cfi.cn/data_ndkA0A1934A1935A1986A1995. ...
- common lisp和scheme的区别
1. 在Common Lisp 眼中,一个符号的symbol-value 和symbol-function 是不一样的,而Scheme对两者不作区分.在Scheme 里面,变量只有唯一对应的值,它可以 ...
- 读书笔记-你不知道的JS上-混入与原型
继承 mixin混合继承 function mixin(obj1, obj2) { for (var key in obj2) { //重复不复制 if (!(key in obj1)) { obj1 ...