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,因为我们要明确的告诉用户是无权访问,而不是未登录。