ASP.NET Core 中基于策略的授权旨在分离授权与应用程序逻辑,它提供了灵活的策略定义模型,在一些权限固定的系统中,使用起来非常方便。但是,当要授权的资源无法预先确定,或需要将权限控制到每一个具体的操作当中时,基于策略的授权便不再适用,本章就来介绍一下如何进行动态的授权。

目录

  1. 基于资源的授权

  2. 基于权限的授权

有些场景下,授权需要依赖于要访问的资源,例如:每个资源通常会有一个创建者属性,我们只允许该资源的创建者才可以对其进行编辑,删除等操作,这就无法通过[Authorize]特性来指定授权了。因为授权过滤器会在我们的应用代码,以及MVC的模型绑定之前执行,无法确定所访问的资源。此时,我们需要使用基于资源的授权,下面就来演示一下具体是如何操作的。

在基于资源的授权中,我们要判断的是用户是否具有针对该资源的某项操作,因此,我们先定义一个代表操作的Requirement

  1. public class MyRequirement : IAuthorizationRequirement
  2. {
  3. public string Name { get; set; }
  4. }

可以根据实际场景来定义需要的属性,在本示例中,只需要一个Name属性,用来表示针对资源的操作名称(如:增查改删等)。

然后,我们预定义一些常用的操作,方便业务中的调用:

  1. public static class Operations
  2. {
  3. public static MyRequirement Create = new MyRequirement { Name = "Create" };
  4. public static MyRequirement Read = new MyRequirement { Name = "Read" };
  5. public static MyRequirement Update = new MyRequirement { Name = "Update" };
  6. public static MyRequirement Delete = new MyRequirement { Name = "Delete" };
  7. }

上面定义的 MyRequirement 虽然很简单,但是非常通用,因此,在 ASP.NET Core 中也内置了一个OperationAuthorizationRequirement

  1. public class OperationAuthorizationRequirement : IAuthorizationRequirement
  2. {
  3. public string Name { get; set; }
  4. }

在实际应用中,我们可以直接使用OperationAuthorizationRequirement,而不需要再自定义 Requirement,而在这里只是为了方便理解,后续也继续使用 MyRequirement 来演示。

每一个 Requirement 都需要有一个对应的 Handler,来完成授权逻辑,可以直接让 Requirement 实现IAuthorizationHandler接口,也可以单独定义授权Handler,在这里使用后者。

在本示例中,我们是根据资源的创建者来判断用户是否具有操作权限,因此,我们定义一个资源创建者的接口,而不是直接依赖于具体的资源:

  1. public interface IDocument
  2. {
  3. string Creator { get; set; }
  4. }

然后实现我们的授权Handler:

  1. public class DocumentAuthorizationHandler : AuthorizationHandler<OperationAuthorizationRequirement, IDocument>
  2. {
  3. protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, IDocument resource)
  4. {
  5. // 如果是Admin角色就直接授权成功
  6. if (context.User.IsInRole("admin"))
  7. {
  8. context.Succeed(requirement);
  9. }
  10. else
  11. {
  12. // 允许任何人创建或读取资源
  13. if (requirement == Operations.Create || requirement == Operations.Read)
  14. {
  15. context.Succeed(requirement);
  16. }
  17. else
  18. {
  19. // 只有资源的创建者才可以修改和删除
  20. if (context.User.Identity.Name == resource.Creator)
  21. {
  22. context.Succeed(requirement);
  23. }
  24. else
  25. {
  26. context.Fail();
  27. }
  28. }
  29. }
  30. return Task.CompletedTask;
  31. }
  32. }

在前面章节的《自定义策略》示例中,我们继承的是AuthorizationHandler<NameAuthorizationRequirement>,而这里继承了AuthorizationHandler<OperationAuthorizationRequirement, Document>,很明显,比之前的多了resource参数,以便用来实现基于资源的授权。

如上,我们并没有验证用户是否已登录,以及context.User是否为空等。这是因为在 ASP.NET Core 的默认授权中,已经对这些进行了判断,我们只需要在要授权的控制器上添加[Authorize]特性即可,无需重复性的工作。

最后,不要忘了,还需要将DocumentAuthorizationHandler注册到DI系统中:

  1. services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();

现在就可以在我们的应用代码中调用IAuthorizationService来完成授权了,不过在此之前,我们再来回顾一下IAuthorizationService接口:

  1. public interface IAuthorizationService
  2. {
  3. Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements);
  4. Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName);
  5. }

在《上一章》中,我们提到,使用[Authorize]设置授权时,其AuthorizationHandlerContext中的resource字段被设置为空,现在,我们将要授权的资源传进去即可:

  1. [Authorize]
  2. public class DocumentsController : Controller
  3. {
  4. public async Task<ActionResult> Details(int? id)
  5. {
  6. var document = _docStore.Find(id.Value);
  7. if (document == null)
  8. {
  9. return NotFound();
  10. }
  11. if ((await _authorizationService.AuthorizeAsync(User, document, Operations.Read)).Succeeded)
  12. {
  13. return View(document);
  14. }
  15. else
  16. {
  17. return new ForbidResult();
  18. }
  19. }
  20. public async Task<IActionResult> Edit(int? id)
  21. {
  22. var document = _docStore.Find(id.Value);
  23. if (document == null)
  24. {
  25. return NotFound();
  26. }
  27. if ((await _authorizationService.AuthorizeAsync(User, document, Operations.Update)).Succeeded)
  28. {
  29. return View(document);
  30. }
  31. else
  32. {
  33. return new ForbidReuslt();
  34. }
  35. }
  36. }

如上,在授权失败时,我们返回了ForbidResult,建议不要返回ChallengeResult,因为我们要明确的告诉用户是无权访问,而不是未登录。

基于资源的权限非常简单,但是每次都要在应用代码中显示调用IAuthorizationService,显然比较繁琐,我们也可以使用AOP模式,或者使用EF Core拦截器来实现,将授权验证与业务代码分离。

在一个通用的用户权限管理系统中,通常每一个Action都代表一种权限,用户拥有哪些权限也是可以动态分配的。本小节就来介绍一下在 ASP.NET Core 中,如何实现一个简单权限管理系统。

首先,我们要确定我们的系统分为哪些权限项,这通常是由业务所决定的,并且是预先确定的,我们可以硬编码在代码中,方便统一调用:

  1. public static class Permissions
  2. {
  3. public const string User = "User";
  4. public const string UserCreate = "User.Create";
  5. public const string UserRead = "User.Read";
  6. public const string UserUpdate = "User.Update";
  7. public const string UserDelete = "User.Delete";
  8. }

如上,我们简单定义了“创建用户”,“查询用户”,“更新用户”,“删除用户”四个权限。通常会对权限项进行分组,构成一个树形结构,这样在展示和配置权限时,都会方便很多。在这里,使用.来表示层级进行分组,其中User权限项包含所有以User.开头的权限。

与基于资源的授权类似,我们同样需要定义一个权限Requirement

  1. public class PermissionAuthorizationRequirement : IAuthorizationRequirement
  2. {
  3. public PermissionAuthorizationRequirement(string name)
  4. {
  5. Name = name;
  6. }
  7. public string Name { get; set; }
  8. }

使用Name属性来表示权限的名称,与上面Permissions的常量对应。

然后实现与上面定义的 Requirement 对应的授权Handler:

  1. public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionAuthorizationRequirement>
  2. {
  3. private readonly UserStore _userStore;
  4. public PermissionAuthorizationHandler(UserStore userStore)
  5. {
  6. _userStore = userStore;
  7. }
  8. protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionAuthorizationRequirement requirement)
  9. {
  10. if (context.User != null)
  11. {
  12. if (context.User.IsInRole("admin"))
  13. {
  14. context.Succeed(requirement);
  15. }
  16. else
  17. {
  18. var userIdClaim = context.User.FindFirst(_ => _.Type == ClaimTypes.NameIdentifier);
  19. if (userIdClaim != null)
  20. {
  21. if (_userStore.CheckPermission(int.Parse(userIdClaim.Value), requirement.Name))
  22. {
  23. context.Succeed(requirement);
  24. }
  25. }
  26. }
  27. }
  28. return Task.CompletedTask;
  29. }
  30. }

如上,把admin角色设置为内部固定角色,直接跳过授权检查。其他角色则从Claims中取出用户Id,然后调用CheckPermission完成授权。

权限检查的具体逻辑就属于业务层面的了,通常会从数据库中查找用的的权限列表进行验证,这里就不在多说,简单模拟了一下:

  1. public class UserStore
  2. {
  3. private static List<User> _users = new List<User>() {
  4. new User { Id=1, Name="admin", Password="111111", Role="admin", Email="admin@gmail.com", PhoneNumber="18800000000"},
  5. new User { Id=2, Name="alice", Password="111111", Role="user", Email="alice@gmail.com", PhoneNumber="18800000001", Permissions = new List<UserPermission> {
  6. new UserPermission { UserId = 1, PermissionName = Permissions.User },
  7. new UserPermission { UserId = 1, PermissionName = Permissions.Role }
  8. }
  9. },
  10. new User { Id=3, Name="bob", Password="111111", Role = "user", Email="bob@gmail.com", PhoneNumber="18800000002", Permissions = new List<UserPermission> {
  11. new UserPermission { UserId = 2, PermissionName = Permissions.UserRead },
  12. new UserPermission { UserId = 2, PermissionName = Permissions.RoleRead }
  13. }
  14. },
  15. };
  16. public bool CheckPermission(int userId, string permissionName)
  17. {
  18. var user = Find(userId);
  19. if (user == null) return false;
  20. return user.Permissions.Any(p => permissionName.StartsWith(p.PermissionName));
  21. }
  22. }

最后,与上面示例一样,将Handler注册到DI系统中:

  1. services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();

那么,怎么在应用代码中使用基于权限的授权呢?

最为简单的,我们可以直接借助于 ASP.NET Core 的授权策略来实现基于权限的授权,因为此时并不需要资源。

  1. services.AddAuthorization(options =>
  2. {
  3. options.AddPolicy(Permissions.UserCreate, policy => policy.AddRequirements(new PermissionAuthorizationRequirement(Permissions.UserCreate)));
  4. options.AddPolicy(Permissions.UserRead, policy => policy.AddRequirements(new PermissionAuthorizationRequirement(Permissions.UserRead)));
  5. options.AddPolicy(Permissions.UserUpdate, policy => policy.AddRequirements(new PermissionAuthorizationRequirement(Permissions.UserUpdate)));
  6. options.AddPolicy(Permissions.UserDelete, policy => policy.AddRequirements(new PermissionAuthorizationRequirement(Permissions.UserDelete)));
  7. });

如上,针对每一个权限项都定义一个对应的授权策略,然后,就可以在控制器中直接使用[Authorize]来完成授权:

  1. [Authorize]
  2. public class UserController : Controller
  3. {
  4. [Authorize(Policy = Permissions.UserRead)]
  5. public ActionResult Index()
  6. {
  7. }
  8. [Authorize(Policy = Permissions.UserRead)]
  9. public ActionResult Details(int? id)
  10. {
  11. }
  12. [Authorize(Policy = Permissions.UserCreate)]
  13. public ActionResult Create()
  14. {
  15. return View();
  16. }
  17. [Authorize(Policy = Permissions.UserCreate)]
  18. [HttpPost]
  19. [ValidateAntiForgeryToken]
  20. public IActionResult Create([Bind("Title")] User user)
  21. {
  22. }
  23. }

当然,我们也可以像基于资源的授权那样,在应用代码中调用IAuthorizationService完成授权,这样做的好处是无需定义策略,但是,显然一个一个来定义策略太过于繁琐。

还有一种更好方式,就是使用MVC过滤器来完成对IAuthorizationService的调用,下面就来演示一下。

我们可以参考上一章中介绍的《AuthorizeFilter》来自定义一个权限过滤器:

  1. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
  2. public class PermissionFilter : Attribute, IAsyncAuthorizationFilter
  3. {
  4. public PermissionFilter(string name)
  5. {
  6. Name = name;
  7. }
  8. public string Name { get; set; }
  9. public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
  10. {
  11. var authorizationService = context.HttpContext.RequestServices.GetRequiredService<IAuthorizationService>();
  12. var authorizationResult = await authorizationService.AuthorizeAsync(context.HttpContext.User, null, new PermissionAuthorizationRequirement(Name));
  13. if (!authorizationResult.Succeeded)
  14. {
  15. context.Result = new ForbidResult();
  16. }
  17. }
  18. }

上面的实现非常简单,我们接受一个name参数,代表权限的名称,然后将权限名称转化为PermissionAuthorizationRequirement,最后直接调用 authorizationService 来完成授权。

接下来,我们就可以直接在控制器中使用PermissionFilter过滤器来完成基于权限的授权了:

  1. [Authorize]
  2. public class UserController : Controller
  3. {
  4. [PermissionFilter(Permissions.UserRead)]
  5. public ActionResult Index()
  6. {
  7. return View(_userStore.GetAll());
  8. }
  9. [PermissionFilter(Permissions.UserCreate)]
  10. public ActionResult Create()
  11. {
  12. }
  13. [PermissionFilter(Permissions.UserCreate)]
  14. [HttpPost]
  15. [ValidateAntiForgeryToken]
  16. public IActionResult Create([Bind("Title")] User user)
  17. {
  18. }
  19. [PermissionFilter(Permissions.UserUpdate)]
  20. public IActionResult Edit(int? id)
  21. {
  22. }
  23. [PermissionFilter(Permissions.UserUpdate)]
  24. [HttpPost]
  25. [ValidateAntiForgeryToken]
  26. public IActionResult Edit(int id, [Bind("Id,Title")] User user)
  27. {
  28. }
  29. }

通常,在前端页面当中,我们也需要根据用户的权限来判断是否显示“添加”,“删除”等按钮,而不是让用户点击“添加”,再提示用户没有权限,这在 ASP.NET Core 中实现起来也非常简单。

我们可以直接在Razor视图中注入IAuthorizationService来检查用户权限:

  1. @inject IAuthorizationService AuthorizationService
  2. @if ((await AuthorizationService.AuthorizeAsync(User, AuthorizationSample.Authorization.Permissions.UserCreate)).Succeeded)
  3. {
  4. <p>
  5. <a asp-action="Create">创建</a>
  6. </p>
  7. }

不过,上面的代码是通过策略名称来授权的,如果我们使用了上面创建的授权过滤器,而没有定义授权策略的话,需要使用如下方式来实现:

  1. @inject IAuthorizationService AuthorizationService
  2. @if ((await AuthorizationService.AuthorizeAsync(User, new PermissionAuthorizationRequirement(AuthorizationSample.Authorization.Permissions.UserCreate))).Succeeded)
  3. {
  4. <p>
  5. <a asp-action="Create">创建</a>
  6. </p>
  7. }

我们也可以定义一个AuthorizationService的扩展方法,实现通过权限名称进行授权,这里就不再多说。

我们不能因为隐藏了操作按钮,就不在后端进行授权验证了,就像JS的验证一样,前端的验证就为了提升用户的体验,后端的验证在任何时候都是必不可少的。

在大多数场景下,我们只需要使用授权策略就可以应对,而在授权策略不能满足我们的需求时,由于 ASP.NET Core 提供了一个统一的 IAuthorizationService 授权接口,这就使我们扩展起来也非常方便。ASP.NET Core 的授权部分到这来也就介绍完了,总的来说,要比ASP.NET 4.x的时候,简单,灵活很多,可见 ASP.NET Core 不仅仅是为了跨平台,而是为了适应现代应用程序的开发方式而做出的全新的设计,我们也应该用全新的思维去学习.NET Core,踏上时代的浪潮。

本文示例代码地址:https://github.com/RainingNight/AspNetCoreSample/tree/master/src/AuthorizationSample

版权声明:本文为RainingNight原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:http://www.cnblogs.com/RainingNight/p/dynamic-authorization-in-asp-net-core.html