Web应用程序开发教程 - 第五章: 授权

关于本教程

在本系列教程中, 你将构建一个名为 Acme.BookStore 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的:

  • Entity Framework Core 做为ORM提供程序.
  • MVC / Razor Pages 做为UI框架.

本教程分为以下部分:

下载源码

本教程根据你的UI数据库偏好有多个版本,我们准备了几种可供下载的源码组合:

如果你在Windows中遇到 “文件名太长” 或 “解压错误”, 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 在Windows 10中启用长路径.

如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path git config --system core.longpaths true

视频教程

本章也被录制为视频教程 发布在YouTube.

权限

ABP框架提供了一个基于ASP.NET Core授权基础架构授权系统. 基于标准授权基础架构的一个主要功能是添加了 权限系统, 这个系统允许定义权限并且根据角色, 用户或客户端启用/禁用权限.

权限名称

权限必须有唯一的名称 (一个 字符串). 最好的方法是把它定义为一个 常量, 这样我们就可以重用这个权限名称了.

打开 Acme.BookStore.Application.Contracts 项目中的 BookStorePermissions 类 (位于 Permissions 文件夹) 并替换为以下代码:

  1. namespace Acme.BookStore.Permissions
  2. {
  3. public static class BookStorePermissions
  4. {
  5. public const string GroupName = "BookStore";
  6. public static class Books
  7. {
  8. public const string Default = GroupName + ".Books";
  9. public const string Create = Default + ".Create";
  10. public const string Edit = Default + ".Edit";
  11. public const string Delete = Default + ".Delete";
  12. }
  13. }
  14. }

权限名称具有层次结构. 例如, “创建图书” 权限被定义为 BookStore.Books.Create. ABP不强制必须如此, 但这是一种有益的做法.

权限定义

在使用权限前必须定义它们.

打开 Acme.BookStore.Application.Contracts 项目中的 BookStorePermissionDefinitionProvider 类 (位于 Permissions 文件夹) 并替换为以下代码:

  1. using Acme.BookStore.Localization;
  2. using Volo.Abp.Authorization.Permissions;
  3. using Volo.Abp.Localization;
  4. namespace Acme.BookStore.Permissions
  5. {
  6. public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider
  7. {
  8. public override void Define(IPermissionDefinitionContext context)
  9. {
  10. var bookStoreGroup = context.AddGroup(BookStorePermissions.GroupName, L("Permission:BookStore"));
  11. var booksPermission = bookStoreGroup.AddPermission(BookStorePermissions.Books.Default, L("Permission:Books"));
  12. booksPermission.AddChild(BookStorePermissions.Books.Create, L("Permission:Books.Create"));
  13. booksPermission.AddChild(BookStorePermissions.Books.Edit, L("Permission:Books.Edit"));
  14. booksPermission.AddChild(BookStorePermissions.Books.Delete, L("Permission:Books.Delete"));
  15. }
  16. private static LocalizableString L(string name)
  17. {
  18. return LocalizableString.Create<BookStoreResource>(name);
  19. }
  20. }
  21. }

这个类定义了一个 权限组 (在UI上分组权限, 下文会看到) 和 权限组中的4个权限. 而且, 创建, 编辑删除BookStorePermissions.Books.Default 权限的子权限. 仅当父权限被选择时, 子权限才能被选择.

最后, 编辑本地化文件 (Acme.BookStore.Domain.Shared 项目的 Localization/BookStore 文件夹中的 en.json) 定义上面使用的本地化键:

  1. "Permission:BookStore": "Book Store",
  2. "Permission:Books": "Book Management",
  3. "Permission:Books.Create": "Creating new books",
  4. "Permission:Books.Edit": "Editing the books",
  5. "Permission:Books.Delete": "Deleting the books"

本地化键名可以是任意的, 并没有强制的规则. 但我们推荐上面使用的约定. 简体中文翻译请打开zh-Hans.json文件 ,并将”Texts”对象中对应的值替换为中文.

权限管理界面

完成权限定义后, 可以在权限管理模态窗口看到它们.

管理 -> Identity -> 角色 页面, 选择admin角色的 权限 操作, 打开权限管理模态窗口:

bookstore-permissions-ui

授予你希望的权限并保存.

提示: 如果运行 Acme.BookStore.DbMigrator 应用程序, 新权限会被自动授予admin.

授权

现在, 你可以使用权限授权图书管理.

应用层 和 HTTP API

打开 the BookAppService 类, 设置策略名称为上面定义的权限名称.

  1. using System;
  2. using Acme.BookStore.Permissions;
  3. using Volo.Abp.Application.Dtos;
  4. using Volo.Abp.Application.Services;
  5. using Volo.Abp.Domain.Repositories;
  6. namespace Acme.BookStore.Books
  7. {
  8. public class BookAppService :
  9. CrudAppService<
  10. Book, //The Book entity
  11. BookDto, //Used to show books
  12. Guid, //Primary key of the book entity
  13. PagedAndSortedResultRequestDto, //Used for paging/sorting
  14. CreateUpdateBookDto>, //Used to create/update a book
  15. IBookAppService //implement the IBookAppService
  16. {
  17. public BookAppService(IRepository<Book, Guid> repository)
  18. : base(repository)
  19. {
  20. GetPolicyName = BookStorePermissions.Books.Default;
  21. GetListPolicyName = BookStorePermissions.Books.Default;
  22. CreatePolicyName = BookStorePermissions.Books.Create;
  23. UpdatePolicyName = BookStorePermissions.Books.Edit;
  24. DeletePolicyName = BookStorePermissions.Books.Delete;
  25. }
  26. }
  27. }

加入代码到构造器. 基类中的 CrudAppService 自动在CRUD操作中使用这些权限. 这不仅实现了 应用服务 的安全性, 也实现了 HTTP API 安全性, 因为如前解释的, HTTP API 自动使用这些服务. (参阅 自动 API controllers).

在稍后开发作者管理功能时, 你将会看到声明式授权, 使用 [Authorize(...)] 特性.

Razor 页面

虽然安全的 HTTP API和应用服务阻止未授权用户使用服务, 但他们依然可以导航到图书管理页面. 虽然当页面发起第一个访问服务器的AJAX请求时会收到授权异常, 但为了更好的用户体验和安全性, 我们应该对页面进行授权.

打开 BookStoreWebModuleConfigureServices 方法中加入以下代码:

  1. Configure<RazorPagesOptions>(options =>
  2. {
  3. options.Conventions.AuthorizePage("/Books/Index", BookStorePermissions.Books.Default);
  4. options.Conventions.AuthorizePage("/Books/CreateModal", BookStorePermissions.Books.Create);
  5. options.Conventions.AuthorizePage("/Books/EditModal", BookStorePermissions.Books.Edit);
  6. });

现在未授权用户会被重定向至登录页面.

隐藏新建图书按钮

图书管理页面有一个 新建图书 按钮, 当用户没有 图书新建 权限时就不可见的.

bookstore-new-book-button-small

打开 Pages/Books/Index.cshtml 文件, 替换内容为以下代码:

  1. @page
  2. @using Acme.BookStore.Localization
  3. @using Acme.BookStore.Permissions
  4. @using Acme.BookStore.Web.Pages.Books
  5. @using Microsoft.AspNetCore.Authorization
  6. @using Microsoft.Extensions.Localization
  7. @model IndexModel
  8. @inject IStringLocalizer<BookStoreResource> L
  9. @inject IAuthorizationService AuthorizationService
  10. @section scripts
  11. {
  12. <abp-script src="/Pages/Books/Index.js"/>
  13. }
  14. <abp-card>
  15. <abp-card-header>
  16. <abp-row>
  17. <abp-column size-md="_6">
  18. <abp-card-title>@L["Books"]</abp-card-title>
  19. </abp-column>
  20. <abp-column size-md="_6" class="text-right">
  21. @if (await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create))
  22. {
  23. <abp-button id="NewBookButton"
  24. text="@L["NewBook"].Value"
  25. icon="plus"
  26. button-type="Primary"/>
  27. }
  28. </abp-column>
  29. </abp-row>
  30. </abp-card-header>
  31. <abp-card-body>
  32. <abp-table striped-rows="true" id="BooksTable"></abp-table>
  33. </abp-card-body>
  34. </abp-card>
  • 加入 @inject IAuthorizationService AuthorizationService 以访问授权服务.
  • 使用 @if (await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create)) 检查图书创建权限, 条件显示 新建图书 按钮.

JavaScript端

图书管理页面中的图书表格每行都有操作按钮. 操作按钮包括 编辑删除 操作:

bookstore-edit-delete-actions

如果用户没有权限, 应该隐藏相关的操作. 表格行中的操作有一个 visible 属性, 可以设置为 false 隐藏操作项.

打开 Acme.BookStore.Web 项目中的 Pages/Books/Index.js, 为 编辑 操作加入 visible 属性:

  1. {
  2. text: l('Edit'),
  3. visible: abp.auth.isGranted('BookStore.Books.Edit'), //CHECK for the PERMISSION
  4. action: function (data) {
  5. editModal.open({ id: data.record.id });
  6. }
  7. }

Delete 操作进行同样的操作:

  1. visible: abp.auth.isGranted('BookStore.Books.Delete')
  • abp.auth.isGranted(...) 检查前面定义的权限.
  • visible 也可以是一个返回 bool 值的函数. 这个函数可以稍后根据某些条件计算.

菜单项

即使我们在图书管理页面的所有层都控制了权限, 应用程序的主菜单依然会显示. 我们应该隐藏用户没有权限的菜单项.

打开 BookStoreMenuContributor 类, 找到下面的代码:

  1. context.Menu.AddItem(
  2. new ApplicationMenuItem(
  3. "BooksStore",
  4. l["Menu:BookStore"],
  5. icon: "fa fa-book"
  6. ).AddItem(
  7. new ApplicationMenuItem(
  8. "BooksStore.Books",
  9. l["Menu:Books"],
  10. url: "/Books"
  11. )
  12. )
  13. );

替换为以下代码:

  1. var bookStoreMenu = new ApplicationMenuItem(
  2. "BooksStore",
  3. l["Menu:BookStore"],
  4. icon: "fa fa-book"
  5. );
  6. context.Menu.AddItem(bookStoreMenu);
  7. //CHECK the PERMISSION
  8. if (await context.IsGrantedAsync(BookStorePermissions.Books.Default))
  9. {
  10. bookStoreMenu.AddItem(new ApplicationMenuItem(
  11. "BooksStore.Books",
  12. l["Menu:Books"],
  13. url: "/Books"
  14. ));
  15. }

你需要为 ConfigureMenuAsync 方法加入 async 关键字, 并重新组织返回值. 最终的 BookStoreMenuContributor 类应该如下:

  1. using System.Threading.Tasks;
  2. using Microsoft.Extensions.DependencyInjection;
  3. using Microsoft.Extensions.Localization;
  4. using Acme.BookStore.Localization;
  5. using Acme.BookStore.MultiTenancy;
  6. using Acme.BookStore.Permissions;
  7. using Volo.Abp.TenantManagement.Web.Navigation;
  8. using Volo.Abp.UI.Navigation;
  9. namespace Acme.BookStore.Web.Menus
  10. {
  11. public class BookStoreMenuContributor : IMenuContributor
  12. {
  13. public async Task ConfigureMenuAsync(MenuConfigurationContext context)
  14. {
  15. if (context.Menu.Name == StandardMenus.Main)
  16. {
  17. await ConfigureMainMenuAsync(context);
  18. }
  19. }
  20. private async Task ConfigureMainMenuAsync(MenuConfigurationContext context)
  21. {
  22. if (!MultiTenancyConsts.IsEnabled)
  23. {
  24. var administration = context.Menu.GetAdministration();
  25. administration.TryRemoveMenuItem(TenantManagementMenuNames.GroupName);
  26. }
  27. var l = context.GetLocalizer<BookStoreResource>();
  28. context.Menu.Items.Insert(0, new ApplicationMenuItem("BookStore.Home", l["Menu:Home"], "~/"));
  29. var bookStoreMenu = new ApplicationMenuItem(
  30. "BooksStore",
  31. l["Menu:BookStore"],
  32. icon: "fa fa-book"
  33. );
  34. context.Menu.AddItem(bookStoreMenu);
  35. //CHECK the PERMISSION
  36. if (await context.IsGrantedAsync(BookStorePermissions.Books.Default))
  37. {
  38. bookStoreMenu.AddItem(new ApplicationMenuItem(
  39. "BooksStore.Books",
  40. l["Menu:Books"],
  41. url: "/Books"
  42. ));
  43. }
  44. }
  45. }
  46. }

下一章

查看本教程的下一章.